Converting Celsius to Fahrenheit round-up
July 22, 2024
0 comments Go, Node, Python, Bun, Ruby, Rust, JavaScript
In the last couple of days, I've created variations of a simple algorithm to demonstrate how Celcius and Fahrenheit seem to relate to each other if you "mirror the number".
It wasn't supposed to be about the programming language. Still, I used Python in the first one and I noticed that since the code is simple, it could be fun to write variants of it in other languages.
- Converting Celsius to Fahrenheit with Python
- Converting Celsius to Fahrenheit with TypeScript
- Converting Celsius to Fahrenheit with Go
- Converting Celsius to Fahrenheit with Ruby
- Converting Celsius to Fahrenheit with Crystal
- Converting Celsius to Fahrenheit with Rust
It was a fun exercise.
And speaking of fun, I couldn't help but to throw in a benchmark using hyperfine
that measures, essentially, how fast these CLIs can start up. The results look like this:
Summary
./conversion-rs ran
1.31 ± 1.30 times faster than ./conversion-go
1.88 ± 1.33 times faster than ./conversion-cr
7.15 ± 4.64 times faster than bun run conversion.ts
14.27 ± 9.48 times faster than python3.12 conversion.py
18.10 ± 12.35 times faster than node conversion.js
67.75 ± 43.80 times faster than ruby conversion.rb
It doesn't prove much, that you didn't expect. But it's fun to see how fast Python 3.12 has become at starting up.
Head on over to https://github.com/peterbe/temperature-conversion to play along. Perhaps you can see some easy optimizations (speed and style).
Node watch mode and TypeScript
July 21, 2024
0 comments Node, JavaScript
You might have heard that Node now has watch mode. It watches the files you're saving and re-runs the node
command automatically. Example:
// example.js
function c2f(c) {
return (c * 9) / 5 + 32;
}
console.log(c2f(0));
Now, run it like this:
❯ node --watch example.js 32 Completed running 'example.js'
Edit that example.js
and the terminal will look like this:
Restarting 'example.js' 32 Completed running 'example.js'
(even if the file didn't change. I.e. you just hit Cmd-S to save)
Now, node
doesn't understand TypeScript natively, yet. So what are you to do: Use @swc-node/register
! (see npmjs here)
You'll need to have a package.json
already or else use globally installed versions.
Example, using npm
:
npm init -y
npm install -D typescript @swc-node/register
npx tsc --init
Now, using:
// example.ts
function c2f(c: number) {
return (c * 9) / 5 + 32;
}
console.log(c2f(123));
You can run it like this:
❯ node --watch --require @swc-node/register example.ts
253.4
Completed running 'example.ts'
Converting Celsius to Fahrenheit with TypeScript
July 16, 2024
0 comments Bun, JavaScript
This is a continuation of Converting Celsius to Fahrenheit with Python, but in TypeScript:
function c2f(c: number): number {
return (c * 9) / 5 + 32;
}
function isMirror(a: number, b: number) {
function massage(n: number) {
if (n < 10) return `0${n}`;
else if (n >= 100) return massage(n - 100);
return `${n}`;
}
return reverseString(massage(a)) === massage(b);
}
function reverseString(str: string) {
return str.split("").reverse().join("");
}
function printConversion(c: number, f: number) {
console.log(`${c}°C ~= ${f}°F`);
}
for (let c = 4; c < 100; c += 12) {
const f = c2f(c);
if (isMirror(c, Math.ceil(f))) {
printConversion(c, Math.ceil(f));
} else if (isMirror(c, Math.floor(f))) {
printConversion(c, Math.floor(f));
} else {
break;
}
}
And when you run it:
❯ bun run conversion.ts
4°C ~= 40°F
16°C ~= 61°F
28°C ~= 82°F
40°C ~= 104°F
52°C ~= 125°F
In TypeScript, how to combine known and unknown keys to an object
July 3, 2024
0 comments JavaScript
More than happy to be informed of a better solution here! But this came up in a real-work situation and I "stumbled" on the solution by more or less guessing.
In plain JavaScript, you have an object which you know you set certain keys on. But because this object is (ab)used for a templating engine, we also put keys/values on it that are not known in advance. In our use case, these keys and booleans came from parsing a .yml
file which. It looks something like this:
// Code simplified for the sake of the example
const context = {
currentVersion: "3.12",
currentLanguage: "en",
activeDate: someDateObject,
// ... other things that are values of type number, bool, Date, and string
// ...
}
if (someCondition()) {
context.hasSomething = true
}
for (const [featureFlag, truth] of Object.entries(parseYamlFile('features.yml')) {
context[featureFlag] = truth
}
const rendered = render(template: { context })
I don't like this design where you "combine" an object with known keys with a spread of unknown keys coming from an external source. But here we are and we have to convert this to TypeScript, the clock's ticking!
In comes TypeScript
Intuitively, from skimming the simplified pseudo-code above you might try this:
type Context = {
currentVersion: string
currentLanguage: string
activeDate: Date
[featureFlag: string]: boolean
}
TypeScript Playground demo here
Except, it won't work:
Property 'currentVersion' of type 'string' is not assignable to 'string' index type 'boolean'. Property 'currentLanguage' of type 'string' is not assignable to 'string' index type 'boolean'. Property 'activeDate' of type 'Date' is not assignable to 'string' index type 'boolean'.
Make sense, right? We're saying the type should have this, that, and that, but also saying that it can be anything. So it's a conflict.
How I solved it
I'll be honest, I'm not sure this is very intuitive, either. But it works:
type FeatureFlags = {
[featureFlag: string]: boolean
}
type Context = FeatureFlags & {
currentVersion: string
currentLanguage: string
activeDate: Date
}
It does imply that the inheritance, using the &
is more than just semantic sugar. It means something.
Simple object lookup in TypeScript
June 14, 2024
2 comments JavaScript
Ever got this error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: string; bar: string; }'. No index signature with a parameter of type 'string' was found on type '{ foo: string; bar: string; }'.(7053)
Yeah, me too. What used to be so simple in JavaScript suddenly feels hard in TypeScript.
In JavaScript,
const greetings = {
good: "Excellent",
bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
alert(greetings[answer] || "OK")
}
To see it in action, I put it into a CodePen.
Now, port that to TypeScript,
const greetings = {
good: "Excellent",
bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
alert(greetings[answer] || "OK")
}
Same. Except it doesn't work.
You can view it here on the TypeScript playground
This is the error you get about greetings[answer]
:
Full error:
Element implicitly has an 'any' type because the expression of type 'string' can't be used to index type '{ good: string; bad: string; }'. No index signature with a parameter of type 'string' was found on type '{ good: string; bad: string; }'.(7053)
The simplest way of saying is that that object greetings
, does not have any keys that are type string
. Instead, the object has keys that are exactly good
and bad
.
I'll be honest, I don't understand the exact details of why it works like this. What I do know is that I want the red squiggly lines to go away and for tsc
to be happy.
But what makes sense, from TypeScript's point of view is that, at runtime the greetings
object can change to be something else. E.g. greetings.bad = 123
and now greetings['bad']
would suddenly be a number. A wild west!
This works:
const greetings: Record<string, string> = {
good: "Excellent",
bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === "string") {
alert(greetings[answer] || "OK")
}
All it does is that it says that the greetings
object is always a strings-to-string object.
See it in the TypeScript playground here
This does not work:
const greetings = {
good: "Excellent",
bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
alert(greetings[answer as keyof greetings] || "OK") // DOES NOT WORK
}
To be able to use as keyof greetings
you need to do that on a type, not on the object. E.g.
This works, but feels more clumsy:
type Greetings = {
good: string
bad: string
}
const greetings: Greetings = {
good: "Excellent",
bad: "Sorry to hear",
}
const answer = prompt("How are you?")
if (typeof answer === 'string') {
alert(greetings[answer as keyof Greetings] || "OK")
}
In conclusion
TypeScript is awesome because it forces you to be more aware of what you're doing. Just because something happen(ed) to work in JavaScript, when you first type it, doesn't mean it will work later.
Note, I still don't know (please enlighten me), what's the best practice between...
const greetings: Record<string, string> = {
...versus...
const greetings: {[key:string]: string} = {
The latter had the advantage that you can give it a name, e.g. "key".
UPDATE (July 1, 2024)
Incorporating Gregor's utility function from the comment below yields this:
function isKeyOfObject<T extends object>(
key: string | number | symbol,
obj: T,
): key is keyof T {
return key in obj;
}
const stuff = {
foo: "Foo",
bar: "Bar"
}
const v = prompt("What are you?")
if (typeof v === 'string') {
console.log("Hello " + (isKeyOfObject(v, stuff) ? stuff[v] : "stranger"))
}
Leibniz formula for π in Python, JavaScript, and Ruby
March 14, 2024
0 comments Python, JavaScript
Officially, I'm one day behind, but here's how you can calculate the value of π using the Leibniz formula.
Python
import math
sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while abs(estimate - math.pi) > epsilon:
sum += (-1) ** i / (2 * i + 1)
estimate = sum * 4
i += 1
print(
f"After {i} iterations, the estimate is {estimate} and the real pi is {math.pi} "
f"(difference of {abs(estimate - math.pi)})"
)
Outputs:
After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)
JavaScript
let sum = 0;
let estimate = 0;
let i = 0;
const epsilon = 0.0001;
while (Math.abs(estimate - Math.PI) > epsilon) {
sum += (-1) ** i / (2 * i + 1);
estimate = sum * 4;
i += 1;
}
console.log(
`After ${i} iterations, the estimate is ${estimate} and the real pi is ${Math.PI} ` +
`(difference of ${Math.abs(estimate - Math.PI)})`
);
Outputs
After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 0.0000999999997586265)
Ruby
sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while (estimate - Math::PI).abs > epsilon
sum += ((-1) ** i / (2.0 * i + 1))
estimate = sum * 4
i += 1
end
print(
"After #{i} iterations, the estimate is #{estimate} and the real pi is #{Math::PI} "+
"(difference of #{(estimate - Math::PI).abs})"
)
Outputs
After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)
Backwards
Technically, these little snippets are checking that it works since each language already has access to a value of π as a standard library constant.
If you don't have that, you can decide on a number of iterations, for example 1,000, and use that.
Python
sum = 0
for i in range(1000):
sum += (-1) ** i / (2 * i + 1)
print(sum * 4)
JavaScript
let sum = 0;
for (const i of [...Array(10000).keys()]) {
sum += (-1) ** i / (2 * i + 1);
}
console.log(sum * 4);
Ruby
sum = 0
for i in 0..10000
sum += ((-1) ** i / (2.0 * i + 1))
end
puts sum * 4
Performance test
Perhaps a bit silly but also a fun thing to play with. Pull out hyperfine
and compare Python 3.12, Node 20.11, Ruby 3.2, and Bun 1.0.30:
❯ hyperfine --warmup 10 "python3.12 ~/pi.py" "node ~/pi.js" "ruby ~/pi.rb" "bun run ~/pi.js"
Benchmark 1: python3.12 ~/pi.py
Time (mean ± σ): 53.4 ms ± 7.5 ms [User: 31.9 ms, System: 12.3 ms]
Range (min … max): 41.5 ms … 64.8 ms 44 runs
Benchmark 2: node ~/pi.js
Time (mean ± σ): 57.5 ms ± 10.6 ms [User: 43.3 ms, System: 11.0 ms]
Range (min … max): 46.2 ms … 82.6 ms 35 runs
Benchmark 3: ruby ~/pi.rb
Time (mean ± σ): 242.1 ms ± 11.6 ms [User: 68.4 ms, System: 37.2 ms]
Range (min … max): 227.3 ms … 265.3 ms 11 runs
Benchmark 4: bun run ~/pi.js
Time (mean ± σ): 32.9 ms ± 6.3 ms [User: 14.1 ms, System: 10.0 ms]
Range (min … max): 17.1 ms … 41.9 ms 60 runs
Summary
bun run ~/pi.js ran
1.62 ± 0.39 times faster than python3.12 ~/pi.py
1.75 ± 0.46 times faster than node ~/pi.js
7.35 ± 1.45 times faster than ruby ~/pi.rb
Comparing Pythons
Just because I have a couple of these installed:
❯ hyperfine --warmup 10 "python3.8 ~/pi.py" "python3.9 ~/pi.py" "python3.10 ~/pi.py" "python3.11 ~/pi.py" "python3.12 ~/pi.py"
Benchmark 1: python3.8 ~/pi.py
Time (mean ± σ): 54.6 ms ± 8.1 ms [User: 33.0 ms, System: 11.4 ms]
Range (min … max): 40.0 ms … 69.7 ms 56 runs
Benchmark 2: python3.9 ~/pi.py
Time (mean ± σ): 54.9 ms ± 8.0 ms [User: 32.2 ms, System: 12.3 ms]
Range (min … max): 42.3 ms … 70.1 ms 38 runs
Benchmark 3: python3.10 ~/pi.py
Time (mean ± σ): 54.7 ms ± 7.5 ms [User: 33.0 ms, System: 11.8 ms]
Range (min … max): 42.3 ms … 78.1 ms 44 runs
Benchmark 4: python3.11 ~/pi.py
Time (mean ± σ): 53.8 ms ± 6.0 ms [User: 32.7 ms, System: 13.0 ms]
Range (min … max): 44.8 ms … 70.3 ms 42 runs
Benchmark 5: python3.12 ~/pi.py
Time (mean ± σ): 53.0 ms ± 6.4 ms [User: 31.8 ms, System: 12.3 ms]
Range (min … max): 43.8 ms … 63.5 ms 42 runs
Summary
python3.12 ~/pi.py ran
1.02 ± 0.17 times faster than python3.11 ~/pi.py
1.03 ± 0.20 times faster than python3.8 ~/pi.py
1.03 ± 0.19 times faster than python3.10 ~/pi.py
1.04 ± 0.20 times faster than python3.9 ~/pi.py