Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions bin/units_from_xls.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ def __call__(self, name_or_suffix):
'''

SHEET_NAME = 'Annex II & Annex III'

# Module-level names for alternate symbols, keyed by (common code, symbol).
# Only four rows in the spreadsheet list more than one symbol, so an explicit
# table is cheaper and more readable than deriving names from the symbol. An
# alternate symbol that is not listed here is still appended to ALL_UNITS and
# so remains reachable through units.Unit('<symbol>'); it just gets no
# module-level name.
ALIAS_UNIT_KEYS = {
('P1', 'pct'): 'PERCENT_PCT',
}

UNIT_KEY_REPLACEMENTS = {
' ': '_',
',': '_',
Expand Down Expand Up @@ -235,11 +246,25 @@ def unit_defs_from_sheet(sheet, column_names):
continue
seen.add(key)

# Split on ' or ' to support the units like '% or pct'
for suffix in suffix.split(' or '):
yield "%s = UnitDescriptor('%s', '%s', '''%s''')\n" % (key, name, code,
suffix)
yield 'ALL_UNITS.append(%s)\n' % key
# Split on ' or ' to support units like '% or pct'. The first symbol
# listed is the primary one and keeps the canonical key. Alternate
# symbols are appended to ALL_UNITS so they stay reachable via
# units.Unit('<suffix>'), but they only get a module-level name when
# ALIAS_UNIT_KEYS says so -- deriving one from the suffix produces
# names like KILOGRAM_PER_LITRE_KG_PER_L that nobody would type.
for idx, suffix in enumerate(suffix.split(' or ')):
if idx == 0:
alias_key = key
else:
alias_key = ALIAS_UNIT_KEYS.get((code, suffix))
if alias_key is None:
yield "ALL_UNITS.append(UnitDescriptor('%s', '%s', '''%s'''))\n" % (
name, code, suffix)
else:
yield "%s = UnitDescriptor('%s', '%s', '''%s''')\n" % (alias_key,
name, code,
suffix)
yield 'ALL_UNITS.append(%s)\n' % alias_key

except xlrd.XLRDError:
sys.stdout.write('Unable to process the .xls file.')
Expand Down
13 changes: 5 additions & 8 deletions openhtf/util/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,7 @@ class UnitDescriptor(
ALL_UNITS.append(KILOGRAM_PER_CUBIC_DECIMETRE)
KILOGRAM_PER_LITRE = UnitDescriptor('kilogram per litre', 'B35', '''kg/l''')
ALL_UNITS.append(KILOGRAM_PER_LITRE)
KILOGRAM_PER_LITRE = UnitDescriptor('kilogram per litre', 'B35', '''kg/L''')
ALL_UNITS.append(KILOGRAM_PER_LITRE)
ALL_UNITS.append(UnitDescriptor('kilogram per litre', 'B35', '''kg/L'''))
CALORIE_THERMOCHEMICAL_PER_GRAM = UnitDescriptor('calorie (thermochemical) per gram', 'B36', '''calth/g''')
ALL_UNITS.append(CALORIE_THERMOCHEMICAL_PER_GRAM)
KILOGRAM_FORCE = UnitDescriptor('kilogram-force', 'B37', '''kgf''')
Expand Down Expand Up @@ -1379,8 +1378,7 @@ class UnitDescriptor(
ALL_UNITS.append(DRY_TON)
DECITONNE = UnitDescriptor('decitonne', 'DTN', '''dt''')
ALL_UNITS.append(DECITONNE)
DECITONNE = UnitDescriptor('decitonne', 'DTN', '''dtn''')
ALL_UNITS.append(DECITONNE)
ALL_UNITS.append(UnitDescriptor('decitonne', 'DTN', '''dtn'''))
DYNE = UnitDescriptor('dyne', 'DU', '''dyn''')
ALL_UNITS.append(DYNE)
PENNYWEIGHT = UnitDescriptor('pennyweight', 'DWT', '''''')
Expand Down Expand Up @@ -2255,8 +2253,7 @@ class UnitDescriptor(
ALL_UNITS.append(FRENCH_GAUGE)
RACK_UNIT = UnitDescriptor('rack unit', 'H80', '''U''')
ALL_UNITS.append(RACK_UNIT)
RACK_UNIT = UnitDescriptor('rack unit', 'H80', '''RU''')
ALL_UNITS.append(RACK_UNIT)
ALL_UNITS.append(UnitDescriptor('rack unit', 'H80', '''RU'''))
MILLIMETRE_PER_MINUTE = UnitDescriptor('millimetre per minute', 'H81', '''mm/min''')
ALL_UNITS.append(MILLIMETRE_PER_MINUTE)
BIG_POINT = UnitDescriptor('big point', 'H82', '''bp''')
Expand Down Expand Up @@ -3647,8 +3644,8 @@ class UnitDescriptor(
ALL_UNITS.append(PAGE_ELECTRONIC)
PERCENT = UnitDescriptor('percent', 'P1', '''%''')
ALL_UNITS.append(PERCENT)
PERCENT = UnitDescriptor('percent', 'P1', '''pct''')
ALL_UNITS.append(PERCENT)
PERCENT_PCT = UnitDescriptor('percent', 'P1', '''pct''')
ALL_UNITS.append(PERCENT_PCT)
COULOMB_PER_METRE = UnitDescriptor('coulomb per metre', 'P10', '''C/m''')
ALL_UNITS.append(COULOMB_PER_METRE)
KILOWEBER = UnitDescriptor('kiloweber', 'P11', '''kWb''')
Expand Down
56 changes: 56 additions & 0 deletions test/util/units_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2026 Google Inc. All Rights Reserved.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for the units module."""

import unittest

from openhtf.util import units


class UnitsTest(unittest.TestCase):

def test_percent_keeps_primary_suffix(self):
"""units.PERCENT must expose '%', not the 'pct' alias."""
self.assertEqual(units.PERCENT.suffix, '%')
self.assertEqual(units.PERCENT_PCT.suffix, 'pct')

def test_primary_symbol_wins_the_canonical_key(self):
"""The first symbol listed in the sheet keeps the module-level name."""
for key, suffix in (
('PERCENT', '%'),
('KILOGRAM_PER_LITRE', 'kg/l'),
('DECITONNE', 'dt'),
('RACK_UNIT', 'U'),
):
self.assertEqual(getattr(units, key).suffix, suffix)

def test_alternate_symbols_stay_reachable_by_lookup(self):
"""Alternates have no module attribute but must still resolve."""
for suffix, name in (
('pct', 'percent'),
('kg/L', 'kilogram per litre'),
('dtn', 'decitonne'),
('RU', 'rack unit'),
):
unit = units.Unit(suffix)
self.assertEqual(unit.suffix, suffix)
self.assertEqual(unit.name, name)

def test_lookup_by_every_suffix(self):
self.assertIs(units.Unit('%'), units.PERCENT)
self.assertIs(units.Unit('pct'), units.PERCENT_PCT)


if __name__ == '__main__':
unittest.main()