Skip to main content

WeatherUnits️ 🌧

Easily convert typical weather units from one unit to another with automatic localization.

Features

🌈 Effortlessly define or extend your own units

☀️ Change nearly every part of unit display from a config.ini file. No need for additional code.

❄️ Localization can is as simple as adding .localize to a units variable or setting it to automatic

Requirements

  • Python 3.10

How to use

📐 Formatting reference → — the format-spec mini-language, every parameter, how precision/digit_budget interact, and the known gotchas. Verified against the library; start there for anything display-related.

Unit Conversion

Conversion is as easy as using the desired unit's name or aliases as either an attribute or subscript

from WeatherUnits.temperature import Fahrenheit
>>> value = Fahrenheit(32)
>>> value
'32°'
>>> value.withUnit
'32°f'
>>> value['c'].withUnit
'0°c'
>>> value.celsius
'0°'

Using a Config file

Loading a config file

import WeatherUnits as wu
wu.config.read('config.ini')

A template config file can be found in src/WeatherUnits/config. There are three sections in a valid config file

  1. LocalUnits: This is what defines what units you want to use for localization
  2. UnitDefaults: This section holds the default values that apply to every unit
  3. UnitProperties: Defines properties for individual units or unit systems as a whole

UnitDefaults

The basic rules are:

  1. Properties are specified without the leading '_'
  2. Units are specified with their lowercase names or types
  3. Empty values will be read as '' and denote being set
  4. Values without an '=' will be read as None and denote being unset

Properties

  • precision: The number of digits allowed to be displayed past the decimal but will not exceed digit_budget. Example number:

    example 3.14159m
    0 3 m
    1 3.1 m
    2 3.14 m
    3 3.142 m

    (The last decimal is rounded, not truncated.)

  • digit_budget: The total number of digits that can be displayed while still showing the decimal. When the value doesn't fit, shorten decides how it is rescaled — either refitting to a different unit or dividing and appending a 10³ suffix.

    example 415.25mm
    2 41.53 cm
    3 415.2 mm
    4 415.2 mm
    5 415.2 mm

    ⚠️ Note the budget caps but does not grant: raising it above 3 does not buy more decimals here, because precision is separately capped by the class default and by the value's own decimal content. The intuitive 4 → 415.3mm, 5 → 415.25mm progression requires raising precision too. See the formatting reference.

  • unit: Override built in unit string

  • suffix: Override built in suffix string.Only used for shortening numbers

  • unit_symbol: Override built in symbol trailing the value.Only used for ° with degrees

  • title: Title to be used by other display programs

  • exp: Override exponent. I can't imagine why this would be used

  • show_unit: Show unit True: 5km False: 5

  • leading_zero: Display zero before values less than 1. True: 0.1in False: .1in auto (default): keeps the zero unless keeping it would cost the value its last meaningful digit — see the formatting reference for the exact rule

  • trailing_zeros: Pad decimals to a stable width instead of the value's own content. By default precision follows the value, so 29.92 shows 29.9 but 30.0 shows 30 — a live readout changes width as it crosses a whole number.

    value inHg(30.0) at precision=2, digit_budget=5
    off (default) 30.0
    precision 30.00
    fill 30.000
    3 30.000

    (At class defaults, with no spec at all, off gives a bare 30 — the derived precision is zero because the value has no decimals of its own. That is the drift this option exists to remove.)

    Always capped by digit_budget, so padding can never push a value over budget. True is an alias for precision.

  • true_zero: Keep an exact zero bare (0) even while trailing_zeros is padding everything else. Default True. A bare 0 says the value is actually zero, where 0.0 says "not zero, but rounds to it at this width" — Inch(0) gives 0 while Inch(0.004) gives 0.0. Set False where a column that never changes width matters more than the distinction, which is most places zero is an ordinary reading: precipitation, wind, lightning strikes. No effect unless trailing_zeros is on.

  • unit_spacer: The text placed between the value and its unit. ⚠️ This is a string, not a flag — False removes it (90°f), but True inserts the literal word (90°Truef). Leave it unset for the class default (a space), or give it the string you want.

  • k_separator: The character separating powers of 10³ — the , in 1,000. Not yet implemented

    value example
    True 1,000ft
    False 1000ft
  • shorten: Rescales a value that doesn't fit the digit_budget. Does one of two things depending on the unit type: refits to a neighbouring unit, or divides and appends a 10³ suffix.

    value example
    True 16.67min
    False 1000s

    (Time.Second(1000) — here it refits the unit. Light.Lux(1000) takes the other branch and gives 1.00k lux.)

  • slide: Scale units for the appropriate values. Not yet implemented

    value example
    True 1km
    False 1000m
  • cardinal: Show degrees as a cardinal direction rather than a number. shorten decides full vs abbreviated name

    value example
    True S
    False 180°

    With cardinal on, two axes work independently: shorten picks the form (letters vs words) and digit_budget picks the detail.

    The budget counts compass components, and the atom it counts depends on the form — a letter when abbreviated, a word when spelled out. So the same budget carries the same amount of information either way:

    digit_budget shorten=True shorten=False resolves to
    1 W West 4 points
    2 SW South West 8 points
    3 WSW West South West 16 points

    The budget is a cap, not a quota: a heading that lands on a cardinal doesn't pad to fill it. At a 2-component budget, 202.5° gives S / South, not a stretched two-component form.

  • key: Key to be used for other programs

  • size_hint: Override generated size hint string. Useful for when you know the expected max string length.

Every code example below is executed against the library as part of verifying this README — they define real, working unit classes.

Defining Your Own Unit

There are currently three types of unit, Static, Scaling, and Derived

Static Unit

The most simple to define. Temperature is an example of a static unit. Similar units of the same are a subclass of the unit type. In the example below, Fahrenheit, Celsius, and Kelvin are subclasses of Temperature. Convert to other similar units with a single private function sharing the name of the desired unit.

from WeatherUnits.base import Measurement, UnitType

@UnitType  # Unit types are defined with this decorator.
class Temperature(Measurement):  # For static unit types, the class inherits Measurement.

    # Shared unit properties are defined here or in a provided config file.
    _unit_symbol = '°'

    # Properties for converting units are defined in the main parent class.
    @property
    def fahrenheit(self):
        # Unless all the subunits are defined in the same file, importing the desired
        # class locally is currently necessary.
        return Fahrenheit(self._fahrenheit())

    # Unit abbreviations are defined somewhere below the converting property.
    f = fahrenheit

    @property
    def celsius(self):
        return Celsius(self._celsius())
    c = celsius

class Fahrenheit(Temperature):
    # Individual unit strings are defined as protected class variables
    # for each unit or within a config file.
    _unit = 'f'

    # This method is what is called to convert Fahrenheit to Celsius.
    def _celsius(self):
        # Since Measurement is a float subclass, math can be done on self.
        return (self - 32) / 1.8

    def _fahrenheit(self):
        # Currently, a function needs to be defined for even converting to the
        # same unit to prevent errors.
        return self

class Celsius(Temperature):
    _unit = 'c'

    def _celsius(self):
        return self

    def _fahrenheit(self):
        return (self * 1.8) + 32

Scaling Unit

Scaling units have quite a bit going on. A scaling unit can be reduced to a single unit through a multiplier, this is the base unit. Multiple unit systems can be defined within one scale. It is also to split systems separate scales as long as there is a function within each system that converts from one base unit to the other.

  • SystemVariant: Marks a unit as falling outside the regular scale
  • Synonym: Denotes that a unit is the same as an already defined one but uses another name. Synonym classes inherit their identically valued class
  • Dimension: A metaclass for the shared dimension class (class Length(metaclass=Dimension, symbol='L')), which the per-system classes then inherit
  • baseUnit=: A class keyword naming the base unit for the system, used when converting to a non-standard SystemVariant
  • system=: A class keyword assigning the class to metric, imperial, or both
  • Scale: Uses the metaclass EnumMeta to define the scaling factors for a ScalingMeasurement class along with the multipliers for any SystemVariants and the base unit

To define a scaling measurement (one that has multiple units within the same unit system, think Centimeter, Meter, Kilometer) the unit system class inherits ScalingMeasurement. The main difference from Measurement is the _Scale class defined inside the main parent class. In the example below, two unit systems (US and SI) are defined in the same scale. Most US units have an SI basis, so this can be done fairly often. However, it is sometimes necessary to split them for readability.

Unified System

from WeatherUnits.base import UnitType, Synonym, ScalingMeasurement
from WeatherUnits.base import Scale, SystemVariant, both

@UnitType
# `baseUnit` names the unit everything reduces to; `system` assigns the class
# to metric, imperial, or both.
class Pressure(ScalingMeasurement, baseUnit='Pascal', system=both):

    # The scale for the unit system is defined with by a class named _Scale inheriting.
    class _Scale(Scale):
        # Every unit is named here starting with the smallest unit and assigned a
        # scaling multiplier.  Scaling multipliers here must be an integer.
        Pascal = 1
        Decapascal = 10
        Hectopascal = 10

        # A base unit is also defined denoted by Base = class.__name__.
        # Base value must be a string.
        Base = 'Pascal'

        # System variants are units that fall outside of the normal scale
        # The multiplier value for variants must be a float and be relative
        # to the base unit.  For example, 1 Bar is 1000 Pascals and
        # 1 MillimeterOfMercury is 1/0.00750062 or 133.3 Pascals.
        Bar = 1000.
        Atmosphere = 101325.
        PoundsPerSquareInch = 6894.757293168
        MillimeterOfMercury = 1 / 0.00750062
        InchOfMercury = 1 / 0.00029530

    @property
    def pascal(self):
        return Pascal(self)

    @property
    def hectopascal(self):
        return Hectopascal(self)

class Pascal(Pressure):
    _unit = 'Pa'

class Hectopascal(Pressure):
    _unit = 'hPa'

class Atmosphere(Pascal, SystemVariant):
    _unit = 'atm'

@Synonym
class Millibar(Hectopascal):
    _unit = 'mBar'
    _digit_budget = 4

Split system

from WeatherUnits.base import ScalingMeasurement, Scale, Dimension
from WeatherUnits.base import metric, imperial


# The shared dimension is a plain class using the Dimension metaclass. It owns
# the conversion properties; the per-system classes below supply the scales.
class Length(metaclass=Dimension, symbol='L'):

    @property
    def meter(self):
        return Meter(self)
    m = meter

    @property
    def foot(self):
        return Foot(self)
    ft = foot


class ImperialLength(ScalingMeasurement, Length, system=imperial, baseUnit='Foot'):

    class _Scale(Scale):
        Line = 1
        Inch = 12
        Foot = 12
        Yard = 3
        Mile = 1760
        Base = 'Foot'

    # Since the multipliers are defined in _Scale, changeScale() uses one
    # parameter to change the scale within the same system.
    def _foot(self):
        return self.changeScale(self._Scale.Foot)

    # This function is called to convert from ImperialLength to MetricLength.  Once its
    # converted to MetricLength, changeScale() is called within the MetricLength class
    # to get to the final unit.
    def _meter(self):
        return self._foot() * 0.3048


class Foot(ImperialLength):
    _unit = 'ft'


class MetricLength(ScalingMeasurement, Length, system=metric, baseUnit='Meter'):

    class _Scale(Scale):
        Millimeter = 1
        Centimeter = 10
        Decimeter = 10
        Meter = 10
        Decameter = 10
        Hectometer = 10
        Kilometer = 10
        Base = 'Meter'

    def _meter(self):
        return self.changeScale(self._Scale.Meter)

    # As with the pervious class, this method is called to convert from MetricLength
    # to ImperialLength.
    def _foot(self):
        return self._meter() * 3.280839895013123


class Meter(MetricLength):
    _unit = 'm'

Derived Unit

DerivedMeasurement is essentially a measurement with multiple units, eg, m/s [meters per second], mph [miles per hour]. Initiation takes in two Measurements and is stored as the dividend of the two. Both values are independently stored as protected variables.

Note: This type still needs quite a lot of work. Currently, it only has fractional support, and only a single unit can be used for both the numerator and denominator. Eventually this will be expanded allowing for more complex unit derivatives. Support for the following has not been added:

  • Multiplying two units together, for example a pascal second [Pa·s]
  • Negative exponent units like Hertz [s⁻¹]
  • Multiple units in either the numerator or denominator

Roughly what DerivedMeasurement does, for orientation (illustrative, not the real source):

# Illustrative sketch, not the real source.
class DerivedMeasurement(Measurement):
    _numerator: Measurement
    _denominator: Measurement

    def __init__(self, numerator, denominator):
        self._numerator = numerator
        self._denominator = denominator
        Measurement.__init__(self, numerator / denominator)

And defining your own:

from WeatherUnits import Length, Time
from WeatherUnits.base import DerivedMeasurement, UnitType


@UnitType
# The numerator and denominator dimensions are declared as class keywords.
class DistanceOverTime(DerivedMeasurement, numerator=Length, denominator=Time):

    # Currently all variations that will be used have to be defined as properties
    @property
    def mih(self):
        converted = DistanceOverTime(self._numerator.mi, self._denominator.hr)

        # Without this override, the unit would be displayed as 'mi/hr'
        converted._suffix = 'mph'
        return converted

    @property
    def ms(self):
        return DistanceOverTime(self._numerator.m, self._denominator.s)

    @property
    def kmh(self):
        return DistanceOverTime(self._numerator.km, self._denominator.hr)

    mph = mih


@UnitType
class Precipitation(DistanceOverTime):

    @property
    def inh(self):
        return Precipitation(self._numerator.inch, self._denominator.hr)

    @property
    def mmh(self):
        return Precipitation(self._numerator.mm, self._denominator.hr)


@UnitType
class PrecipitationRate(Precipitation):
    pass


# Fixing the denominator is a one-liner: no __init__ override needed.
class Daily(PrecipitationRate, denominator=Time.Day): ...


class Hourly(PrecipitationRate, denominator=Time.Hour): ...

Metadata

Release files for WeatherUnits 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for WeatherUnits 0.8.0
File Size Uploaded
weatherunits-0.8.0.tar.gz 63.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for WeatherUnits 0.8.0
File Interpreter ABI Platform
weatherunits-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 135.5 kB

Release files / weatherunits-0.8.0.tar.gz

Download URL weatherunits-0.8.0.tar.gz
Size 63.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8939ed4c061af413614e6ef81b20442133c0ec45b6ed926cb93df5928cf41470
BLAKE2b-256 checksum
How to use checksums
60f5370cf4c50f2079c6ecd03648d2c1a19ca169397c9bed643d502c2e947683
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.12.14 Darwin/27.2.0

Release files / weatherunits-0.8.0-py3-none-any.whl

Download URL weatherunits-0.8.0-py3-none-any.whl
Size 72.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a5cb2e73da8ca766f176b4ab4680313e5759573b8ebd213eeb2d6fb808399364
BLAKE2b-256 checksum
How to use checksums
d943be950158e8740080739858fa85af19ef66f56bc2610841635ca74855ed4e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.12.14 Darwin/27.2.0

Release history Release notifications | RSS feed

This release

0.8.0 This release

2 release files

0.7.1

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.0

2 release files

0.3.1

1 release file

0.3

1 release file

0.2.4

1 release file

0.2.2

1 release file

0.2.1

1 release file

0.2

1 release file

0.1.2

1 release file

0.1.1

1 release file

0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page