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).
Converting Celsius to Fahrenheit with Python
July 12, 2024
0 comments Python
Here's a useful mnemonic for remembering how to convert Celsius to Fahrenhait(*):
- Start at 4°C
- Add +12 each time
- Flip the C in mirror, with some additional fudging
For example, 4°C is 04°C. Mirror image of "04" is "40". So 4°C equals 40°F.
And when there's a 1 in front, as in 125°F, look at that as 100 + 25°F. Mirror of 25°F is 52°C. So 52°C equals 125°F.
In Python it can be tested like this:
import math
def c2f(c):
return c * 9 / 5 + 32
def is_mirror(a, b):
def massage(n):
if n < 10:
return f"0{n}"
elif n >= 100:
return massage(n - 100)
else:
return str(n)
return massage(a)[::-1] == massage(b)
def print_conv(c, f):
print(f"{c}°C ~= {f}°F")
for i in range(4, 100, 12):
f = c2f(i)
if is_mirror(i, math.ceil(f)):
print_conv(i, math.ceil(f))
elif is_mirror(i, math.floor(f)):
print_conv(i, math.floor(f))
else:
break
When you run that you get:
4°C ~= 40°F 16°C ~= 61°F 28°C ~= 82°F 40°C ~= 104°F 52°C ~= 125°F
(*) If you can't remember F = C × 9/5 + 32 or, perhaps, remember it but can't compute the arithmetic easily.
How do you thousands-comma AND whitespace format a f-string in Python
March 17, 2024
1 comment Python
For some reason, I always forget how to do this. Tired of that. Let's blog about it so it sticks.
To format a number with thousand-commas you do:
>>> n = 1234567
>>> f"{n:,}"
'1,234,567'
To add whitespace to a string you do:
>>> name="peter"
>>> f"{name:<20}"
'peter '
How to combine these in one expression, you do:
>>> n = 1234567
>>> f"{n:<15,}"
'1,234,567 '
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
How to avoid a count query in Django if you can
February 14, 2024
1 comment Django, Python
Suppose you have a complex Django QuerySet query that is somewhat costly (in other words slow). And suppose you want to return:
- The first N results
- A count of the total possible results
So your implementation might be something like this:
def get_results(queryset, fields, size):
count = queryset.count()
results = []
for record in queryset.values(*fields)[:size]
results.append(record)
return {"count": count, "results": results}
That'll work. If there are 1,234 rows in your database table that match those specific filters, what you might get back from this is:
>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
1234
>>> len(results["results"])
5
Or, if the filters would only match 3 rows in your database table:
>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
3
>>> len(results["results"])
3
Between your Python application and your database you'll see:
query 1: SELECT COUNT(*) FROM my_database WHERE ... query 2: SELECT name, age FROM my_database WHERE ... LIMIT 5
The problem with this is that, in the latter case, you had to send two database queries when all you needed was one.
If you knew it would only match a tiny amount of records, you could do this:
def get_results(queryset, fields, size):
- count = queryset.count()
results = []
for record in queryset.values(*fields)[:size]:
results.append(record)
+ count = len(results)
return {"count": count, "results": results}
But that is wrong. The count
would max out at whatever the size
is.
The solution is to try to avoid the potentially unnecessary .count()
query.
def get_results(queryset, fields, size):
count = 0
results = []
for i, record in enumerate(queryset.values(*fields)[: size + 1]):
if i == size:
# Alas, there are more records than the pagination
count = queryset.count()
break
count = i + 1
results.append(record)
return {"count": count, "results": results}
This way, you only incur one database query when there wasn't that much to find, but if there was more than what the pagination called for, you have to incur that extra database query.
Pip-Outdated.py with interactive upgrade
September 21, 2023
0 comments Python
Last year I wrote a nifty script called Pip-Outdated.py
"Pip-Outdated.py - a script to compare requirements.in with the output of pip list --outdated". It basically runs pip list --outdated
but filters based on the packages mentioned in your requirements.in
. For people familiar with Node, it's like checking all installed packages in node_modules
if they have upgrades, but filter it down by only those mentioned in your package.json
.
I use this script often enough that I added a little interactive input to ask if it should edit requirements.in
for you for each possible upgrade. Looks like this:
❯ Pip-Outdated.py
black INSTALLED: 23.7.0 POSSIBLE: 23.9.1
click INSTALLED: 8.1.6 POSSIBLE: 8.1.7
elasticsearch-dsl INSTALLED: 7.4.1 POSSIBLE: 8.9.0
fastapi INSTALLED: 0.101.0 POSSIBLE: 0.103.1
httpx INSTALLED: 0.24.1 POSSIBLE: 0.25.0
pytest INSTALLED: 7.4.0 POSSIBLE: 7.4.2
Update black from 23.7.0 to 23.9.1? [y/N/q] y
Update click from 8.1.6 to 8.1.7? [y/N/q] y
Update elasticsearch-dsl from 7.4.1 to 8.9.0? [y/N/q] n
Update fastapi from 0.101.0 to 0.103.1? [y/N/q] n
Update httpx from 0.24.1 to 0.25.0? [y/N/q] n
Update pytest from 7.4.0 to 7.4.2? [y/N/q] y
and then,
❯ git diff requirements.in | cat
diff --git a/requirements.in b/requirements.in
index b7a246e..0e996e5 100644
--- a/requirements.in
+++ b/requirements.in
@@ -9,7 +9,7 @@ python-decouple==3.8
fastapi==0.101.0
uvicorn[standard]==0.23.2
selectolax==0.3.16
-click==8.1.6
+click==8.1.7
python-dateutil==2.8.2
gunicorn==21.2.0
# I don't think this needs `[secure]` because it's only used by
@@ -18,7 +18,7 @@ requests==2.31.0
cachetools==5.3.1
# Dev things
-black==23.7.0
+black==23.9.1
flake8==6.1.0
-pytest==7.4.0
+pytest==7.4.2
httpx==0.24.1
That's it. Then if you want to actually make these upgrades you run:
❯ pip-compile --generate-hashes requirements.in && pip install -r requirements.txt
To install it, download the script from: https://gist.github.com/peterbe/a2b158c39f1f835c0977c82befd94cdf
and put it in your ~/bin
and make it executable.
Now go into a directory that has a requirements.in
and run Pip-Outdated.py