2
\$\begingroup\$

ToTemp is a temperature conversion package with Celsius, Delisle, Fahrenheit, Kelvin, Rankine, Réaumur, Newton and Rømer scales. With a documentation and already in PyPI.

The source code for the main implementation here, since is too long to be completely shown, it is basically a ABC for Abstract Temperature Scales, which all scales inhehit from.

Here's a snippet of it (ommited most methods and docs for brevity, besides for __init_subclass__, __init__, __repr__, __str__, __add__, convert_to and to_fahrenheit):

from __future__ import annotations
from abc import ABCMeta, abstractmethod
from typing import Any, ClassVar, TypeVar
T = TypeVar('T', bound='AbstractTemperature')
class AbstractTemperature(metaclass=ABCMeta):
 """
 ...
 """
 _symbol: ClassVar[str]
 _value: float
 @classmethod
 def __init_subclass__(cls, **kwargs: object) -> None:
 """Ensures subclasses set the `_symbol` attribute."""
 super().__init_subclass__(**kwargs)
 try:
 _ = cls._symbol
 except AttributeError:
 raise AttributeError(
 'Temperature subclasses must set the `_symbol` class attribute'
 ) from None
 def __init__(self, value: float) -> None:
 self._value = value
 def __str__(self) -> str:
 """
 ...
 """
 return f'{self._value} {self._symbol}'
 def __repr__(self) -> str:
 """
 ...
 """
 return f'{self.__class__.__name__}({self._value})'
 def __add__(self: T, other: Any) -> T:
 """
 Returns a new instance of the same class with the sum of the values.
 If `other` is a temperature instance, it is first converted to the
 calling class, then the values are added.
 Otherwise, an attempt is made to add `other` to the value directly.
 Notes
 -----
 If `other` is not a temperature instance, atempts to return: cls(self._value + other)
 Returns
 -------
 self.__add__(other) : T
 cls(self._value + other.convert_to(cls).value)
 """
 cls = self.__class__
 try:
 if isinstance(other, AbstractTemperature):
 return cls(self._value + other.convert_to(cls).value)
 return cls(self._value + other)
 except TypeError:
 return NotImplemented
 def to_fahrenheit(self) -> Fahrenheit:
 """
 Returns a Fahrenheit object which contains the class attribute "value"
 with the result from the conversion typed the same as the attribute.
 Returns
 -------
 convert_to(Fahrenheit) : Fahrenheit
 """
 return self.convert_to(Fahrenheit)
 @abstractmethod
 def convert_to(self, temp_cls: type[T]) -> T:
 """
 Returns an instance of `temp_cls` containing the converted value.
 If no conversion to `temp_cls` is possible, `TypeError` is raised.
 """
 ...

This package aims to bring the simple and straight to the point, but precise, Object Oriented experience of working with temperature scale data types.

First of all, install the package:

pip install totemp

The instances:

from totemp import Celsius, Fahrenheit
if __name__ == '__main__':
 temps: list = [Celsius(12), Celsius(25), Celsius(50)]
 print(temps[0]) # '12 oC'
 print(temps) # [Celsius(12), Celsius(25), Celsius(50)]
 temps = list(map(Celsius.to_fahrenheit, temps))
 print(temps[0]) # '53.6 oF'
 print(temps) # [Fahrenheit(53.6), Fahrenheit(77.0), Fahrenheit(122.0)]

It's representations and properties:

Property symbol is read-only.

from totemp import Fahrenheit
if __name__ == '__main__':
 temp0 = Fahrenheit(53.6)
 print(temp0.__repr__()) # 'Fahrenheit(53.6)'
 print(temp0.__str__()) # '53.6 oF'
 print(temp0.symbol) # 'oF'
 print(temp0.value) # 53.6

Comparision operations ('==', '!=', '>', '>=', '<',...):

The comparision/arithmetic implementation attempts to convert the value of other (if it is a temperature instance) and then evaluate the expression.

import totemp as tp
if __name__ == '__main__':
 temp0, temp1 = tp.Celsius(0), tp.Fahrenheit(32)
 print(f'temp0: {repr(temp0)}') # Celsius(0)
 print(f'temp1: {repr(temp1.to_celsius())}') # Celsius(0.0)
 print(temp0 != temp1) # False
 print(temp0 > temp1) # False
 print(temp0 < temp1) # False
 print(temp0 >= temp1) # True
 print(temp0 <= temp1) # True
 print(temp0 == temp1) # True

Arithmetic operations ('+', '-', '*', '**', '/', '//', '%', ...):

from totemp import Newton, Rankine
if __name__ == '__main__':
 temp0 = Newton(33)
 temp1 = Rankine(671.67)
 temp2 = temp0 + temp1
 print('temp2:', temp2) # temp2: 65.99999999999999 oN
 print('temp2:', repr(temp2)) # temp2: Newton(65.99999999999999)
 print('temp2:', temp2.value, temp2.symbol) # temp2: 65.99999999999999 oN
 print((temp0 + temp1).rounded()) # 66 oN
 print(repr((temp0 + temp1).rounded())) # Newton(66)
 print(temp2 + 12.55) # 78.54999999999998 oN
 print((12 + temp2.rounded())) # 78 oN

ToTemp classes can work with many built-in Python functions:

from math import floor, ceil, trunc
from totemp import Reaumur
if __name__ == '__main__':
 temp = Reaumur(100.4)
 float(temp) # 100.4
 int(temp) # 100
 round(temp) # Reaumur(100)
 abs(temp) # Reaumur(100)
 floor(temp) # Reaumur(100)
 ceil(temp) # Reaumur(101)
 trunc(temp) # Reaumur(100)
 divmod(temp, temp0 := Reaumur(25.1)) # (Reaumur(4.0), Reaumur(0.0))

Temperature Instance Conversions:

import totemp
if __name__ == '__main__':
 temp = totemp.Fahrenheit(32)
 print(temp.to_celsius()) # 0.0 oC
 print(temp.to_fahrenheit()) # 32 oF
 print(temp.to_delisle()) # 150.0 oDe
 print(temp.to_kelvin()) # 273.15 K
 print(temp.to_newton()) # 0.0 oN
 print(temp.to_rankine()) # 491.67 oR
 print(temp.to_reaumur()) # 0.0 oRé
 print(temp.to_romer()) # 7.5 oRø

And that's it, that's my first ever project, some feedback or collaborations would be wonderfull! Proud of doing this, always gratefull for the friends and other programmer dudes of the internet that helped me make this project happen.

asked Mar 3, 2023 at 18:01
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Tests

You test that a value in corresponds to an expected value out, but where did you get the calculated value out? Was it from your code? If so, it's only testing regression.

I'd prefer to see some tests based on the scales' definitions themselves (i.e. freezing/boiling pt of water) are in agreement, along with some other known quantities (e.g. 0K to check nothing reasonably goes below 0K on conversion [though this is possible in limited contexts unlikely to occur here], human body temp, other melting/boiling points) as well as some arbitrary points on the scale for regression testing.

You should also probably do conversion there and back i.e. degC -> degF -> degC to check that both formulae are implemented correctly with tolerable accuracy.

You also only test coversions, not any of your comparisons or arithmetic ops.

Your test function takes a strange input of a 4-tuple, which is really two 2-tuples to be compared. This would make much more sense to me as a map (dict) of input->output, this also makes them iterable as pairs e.g.

def func_to_test_precise_rounded_results(mapping):
 for inp, outp in mapping.items():
 if inp != outp: # Rather than `not inp == outp`
 errors.append(f'{inp} != {outp} -> should be equal')

This could then be called as:

temps = {
 Romer(25).to_newton(): Newton(11.0),
 Romer(25).to_newton().rounded(): Newton(11),
 }
func_to_test_precise_rounded_results(temps)

Which, to me, makes the intent more clear and also allows testing as many comparisons as you'd like.

Also, to me, the name func_to_test_precise_rounded_results is overly verbose with no extra detail. We know it's a func because we give it a verb name. It doesn't test precise vs rounded results, because those are manually provided. It compares the first 2 and last 2 elements of a 4-tuple.

Implementation

A common way to do lots of conversions is to have an internal value which serves as your standard format, say you picked K. You would then convert any value stored to K and then back out to unit. This means instead of having to implement new converters for every unit to every other:

C -> F, F -> C, C -> K, K -> C, F -> K, K -> F [combinatoric growth]

You just need:

C -> K, F -> K, K -> C, K -> F [2N-1]

Then C -> F is implemented as C -> K -> F. This does mean you can lose some accuracy over repeated operations, but in general this doesn't cause too much of a problem.

Arithmetic operations

You have implemented arithmetic operations, but what does an arithmetic operation mean on a physical quantity? 3m * 3m != 9m! It's 9m^2. Likewise for division and who knows what a mod (%) does to the units.

Misc

You have slightly inconsistent renderings of some of your formulae which made me have to double check whether they were right

class Celsius(AbstractTemperature):
 ...
 def convert_to(self, temp_cls: type[T]) -> T:
 ...
 if temp_cls is Newton:
 return temp_cls(self._value * 33 / 100)
class Newton(AbstractTemperature):
 ...
 def convert_to(self, temp_cls: type[T]) -> T:
 if temp_cls is Celsius:
 return temp_cls(self._value / 0.33)

Consistency of style can be very helpful.

Converting to Newton scale is unlikely to be consistent across the board as it's a relatively simple scale with 2 different metrics.

Some points

  1. 0degC = 0degN
  2. 100degC = 33degN (where you get 1/3)
  3. 271degC = 81degN (81*3 = 243) - Melting pt of bismuth
  4. 327degC = 96degN (96*3 = 288) - Melting pt of lead
answered Mar 4, 2023 at 13:12
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.