Filtered by JavaScript

Reset

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.

  1. Converting Celsius to Fahrenheit with Python
  2. Converting Celsius to Fahrenheit with TypeScript
  3. Converting Celsius to Fahrenheit with Go
  4. Converting Celsius to Fahrenheit with Ruby
  5. Converting Celsius to Fahrenheit with Crystal
  6. 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

Speed comparison

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
}

TS error

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
}

TypeScript Playground demo

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]:

TypeScript playground with error

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"))
}

TypeScript Playground demo

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.

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
Previous page
Next page