diff --git a/pint/babel_names.py b/pint/babel_names.py new file mode 100644 index 0000000..45601bd --- /dev/null +++ b/pint/babel_names.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" + pint.babel + ~~~~~~~~~~ + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from pint.compat import HAS_PROPER_BABEL + +_babel_units = dict( + standard_gravity='acceleration-g-force', + millibar='pressure-millibar', + metric_ton='mass-metric-ton', + megawatt='power-megawatt', + degF='temperature-fahrenheit', + dietary_calorie='energy-foodcalorie', + millisecond='duration-millisecond', + mph='speed-mile-per-hour', + acre_foot='volume-acre-foot', + mebibit='digital-megabit', + gibibit='digital-gigabit', + tebibit='digital-terabit', + mebibyte='digital-megabyte', + kibibyte='digital-kilobyte', + mm_Hg='pressure-millimeter-of-mercury', + month='duration-month', + kilocalorie='energy-kilocalorie', + cubic_mile='volume-cubic-mile', + arcsecond='angle-arc-second', + byte='digital-byte', + metric_cup='volume-cup-metric', + kilojoule='energy-kilojoule', + meter_per_second_squared='acceleration-meter-per-second-squared', + pint='volume-pint', + square_centimeter='area-square-centimeter', + in_Hg='pressure-inch-hg', + milliampere='electric-milliampere', + arcminute='angle-arc-minute', + MPG='consumption-mile-per-gallon', + hertz='frequency-hertz', + day='duration-day', + mps='speed-meter-per-second', + kilometer='length-kilometer', + square_yard='area-square-yard', + kelvin='temperature-kelvin', + kilogram='mass-kilogram', + kilohertz='frequency-kilohertz', + megahertz='frequency-megahertz', + meter='length-meter', + cubic_inch='volume-cubic-inch', + kilowatt_hour='energy-kilowatt-hour', + second='duration-second', + yard='length-yard', + light_year='length-light-year', + millimeter='length-millimeter', + metric_horsepower='power-horsepower', + gibibyte='digital-gigabyte', + ## 'temperature-generic', + liter='volume-liter', + turn='angle-revolution', + microsecond='duration-microsecond', + pound='mass-pound', + ounce='mass-ounce', + calorie='energy-calorie', + centimeter='length-centimeter', + inch='length-inch', + centiliter='volume-centiliter', + troy_ounce='mass-ounce-troy', + gream='mass-gram', + kilowatt='power-kilowatt', + knot='speed-knot', + lux='light-lux', + hectoliter='volume-hectoliter', + microgram='mass-microgram', + degC='temperature-celsius', + tablespoon='volume-tablespoon', + cubic_yard='volume-cubic-yard', + square_foot='area-square-foot', + tebibyte='digital-terabyte', + square_inch='area-square-inch', + carat='mass-carat', + hectopascal='pressure-hectopascal', + gigawatt='power-gigawatt', + watt='power-watt', + micrometer='length-micrometer', + volt='electric-volt', + bit='digital-bit', + gigahertz='frequency-gigahertz', + teaspoon='volume-teaspoon', + ohm='electric-ohm', + joule='energy-joule', + cup='volume-cup', + square_mile='area-square-mile', + nautical_mile='length-nautical-mile', + square_meter='area-square-meter', + mile='length-mile', + acre='area-acre', + nanometer='length-nanometer', + hour='duration-hour', + astronomical_unit='length-astronomical-unit', + liter_per_100kilometers ='consumption-liter-per-100kilometers', + megaliter='volume-megaliter', + ton='mass-ton', + hectare='area-hectare', + square_kilometer='area-square-kilometer', + kibibit='digital-kilobit', + mile_scandinavian='length-mile-scandinavian', + liter_per_kilometer='consumption-liter-per-kilometer', + century='duration-century', + cubic_foot='volume-cubic-foot', + deciliter='volume-deciliter', + ##pint='volume-pint-metric', + cubic_meter='volume-cubic-meter', + cubic_kilometer='volume-cubic-kilometer', + quart='volume-quart', + cc='volume-cubic-centimeter', + pound_force_per_square_inch='pressure-pound-per-square-inch', + milligram='mass-milligram', + kph='speed-kilometer-per-hour', + minute='duration-minute', + parsec='length-parsec', + picometer='length-picometer', + degree='angle-degree', + milliwatt='power-milliwatt', + week='duration-week', + ampere='electric-ampere', + milliliter='volume-milliliter', + decimeter='length-decimeter', + fluid_ounce='volume-fluid-ounce', + nanosecond='duration-nanosecond', + foot='length-foot', + karat='proportion-karat', + year='duration-year', + gallon='volume-gallon', + radian='angle-radian', +) + +if not HAS_PROPER_BABEL: + _babel_units = dict() + +_babel_systems = dict( + mks='metric', + imperial='uksystem', + US='ussystem', +) + +_babel_lengths = ['narrow', 'short', 'long'] + diff --git a/pint/compat/__init__.py b/pint/compat/__init__.py index 1839baf..05cd086 100644 --- a/pint/compat/__init__.py +++ b/pint/compat/__init__.py @@ -118,3 +118,13 @@ except ImportError: ufloat = None HAS_UNCERTAINTIES = False +try: + from babel import Locale as Loc + from babel import units as babel_units + HAS_BABEL = True + HAS_PROPER_BABEL = hasattr(babel_units, 'format_unit') +except ImportError: + HAS_PROPER_BABEL = HAS_BABEL = False + +if not HAS_PROPER_BABEL: + Loc = babel_units = None diff --git a/pint/formatting.py b/pint/formatting.py index b1ebf43..d999174 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -13,6 +13,9 @@ from __future__ import division, unicode_literals, print_function, absolute_impo import re +from .babel_names import _babel_units, _babel_lengths +from pint.compat import babel_units, Loc + __JOIN_REG_EXP = re.compile("\{\d*\}") @@ -100,7 +103,8 @@ _FORMATS = { def formatter(items, as_ratio=True, single_denominator=False, product_fmt=' * ', division_fmt=' / ', power_fmt='{0} ** {1}', - parentheses_fmt='({0})', exp_call=lambda x: '{0:n}'.format(x)): + parentheses_fmt='({0})', exp_call=lambda x: '{0:n}'.format(x), + locale=None, babel_length='long', babel_plural_form='one'): """Format a list of (name, exponent) pairs. :param items: a list of (name, exponent) pairs. @@ -111,6 +115,9 @@ def formatter(items, as_ratio=True, single_denominator=False, :param division_fmt: the format used for division. :param power_fmt: the format used for exponentiation. :param parentheses_fmt: the format used for parenthesis. + :param locale: the locale object as defined in babel. + :param babel_length: the length of the translated unit, as defined in babel cldr. + :param babel_plural_form: the plural form, calculated as defined in babel. :return: the formula as a string. """ @@ -126,6 +133,28 @@ def formatter(items, as_ratio=True, single_denominator=False, pos_terms, neg_terms = [], [] for key, value in sorted(items): + if locale and babel_length and babel_plural_form and key in _babel_units: + _key = _babel_units[key] + locale = Loc.parse(locale) + unit_patterns = locale._data['unit_patterns'] + compound_unit_patterns = locale._data["compound_unit_patterns"] + plural = 'one' if abs(value) <= 0 else babel_plural_form + if babel_length not in _babel_lengths: + other_lengths = [ + _babel_length for _babel_length in reversed(_babel_lengths) \ + if babel_length != _babel_length + ] + else: + other_lengths = [] + for _babel_length in [babel_length] + other_lengths: + pat = unit_patterns.get(_key, {}).get(_babel_length, {}).get(plural) + print(plural, _babel_length, pat) + if pat is not None: + key = pat.replace('{0}', '').strip() + break + division_fmt = compound_unit_patterns.get("per", {}).get(babel_length, division_fmt) + power_fmt = '{0}{1}' + exp_call = _pretty_fmt_exponent if value == 1: pos_terms.append(key) elif value > 0: @@ -177,12 +206,13 @@ def _parse_spec(spec): return result -def format_unit(unit, spec): +def format_unit(unit, spec, **kwspec): if not unit: return 'dimensionless' spec = _parse_spec(spec) - fmt = _FORMATS[spec] + fmt = dict(_FORMATS[spec]) + fmt.update(kwspec) if spec == 'L': rm = [(r'\mathrm{{{0}}}'.format(u), p) for u, p in unit.items()] diff --git a/pint/quantity.py b/pint/quantity.py index 629f22c..980ff10 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -26,6 +26,7 @@ from .compat import string_types, ndarray, np, _to_magnitude, long_type from .util import (logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, fix_str_conversions) +from pint.compat import Loc def _eq(first, second, check_all): @@ -171,6 +172,24 @@ class _Quantity(SharedRegistryObject): ustr = ustr[2:] return allf.format(mstr, ustr).strip() + def format_babel(self, spec='', **kwspec): + spec = spec or self.default_format + + # standard cases + if '#' in spec: + spec = spec.replace('#', '') + obj = self.to_compact() + else: + obj = self + kwspec = dict(kwspec) + if 'length' in kwspec: + kwspec['babel_length'] = kwspec.pop('length') + kwspec['locale'] = Loc.parse(kwspec['locale']) + kwspec['babel_plural_form'] = kwspec['locale'].plural_form(obj.magnitude) + return '{0} {1}'.format( + format(obj.magnitude, remove_custom_flags(spec)), + obj.units.format_babel(spec, **kwspec)).replace('\n', '') + # IPython related code def _repr_html_(self): return self.__format__('H') diff --git a/pint/systems.py b/pint/systems.py index 126b046..e2d5564 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -16,6 +16,8 @@ import re from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError, RedefinitionError from .util import to_units_container, SharedRegistryObject, SourceIterator +from .babel_names import _babel_systems +from pint.compat import Loc class _Group(SharedRegistryObject): @@ -336,6 +338,17 @@ class _System(SharedRegistryObject): self.invalidate_members() + def format_babel(self, locale): + """translate the name of the system + + :type locale: Locale + """ + if locale and self.name in _babel_systems: + name = _babel_systems[self.name] + locale = Loc.parse(locale) + return locale.measurement_systems[name] + return self.name + @classmethod def from_lines(cls, lines, get_root_func): lines = SourceIterator(lines) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index f17e397..bb493b6 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -7,7 +7,7 @@ import doctest from distutils.version import StrictVersion import re -from pint.compat import HAS_NUMPY, HAS_UNCERTAINTIES, NUMPY_VER, PYTHON3 +from pint.compat import HAS_NUMPY, HAS_PROPER_BABEL, HAS_UNCERTAINTIES, NUMPY_VER, PYTHON3 from pint.testsuite.compat import unittest @@ -31,6 +31,10 @@ def requires_not_numpy(): return unittest.skipIf(HAS_NUMPY, 'Requires NumPy is not installed.') +def requires_proper_babel(): + return unittest.skipUnless(HAS_PROPER_BABEL, 'Requires Babel with units support') + + def requires_uncertainties(): return unittest.skipUnless(HAS_UNCERTAINTIES, 'Requires Uncertainties') diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py new file mode 100644 index 0000000..0194977 --- /dev/null +++ b/pint/testsuite/test_babel.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import division, unicode_literals, print_function, absolute_import + +from pint.testsuite import helpers, BaseTestCase +from pint import UnitRegistry +import os + +class TestBabel(BaseTestCase): + + @helpers.requires_proper_babel() + def test_babel(self): + ureg = UnitRegistry() + dirname = os.path.dirname(__file__) + ureg.load_definitions(os.path.join(dirname, '../xtranslated.txt')) + + distance = 24.0 * ureg.meter + self.assertEqual( + distance.format_babel(locale='fr_FR', length='long'), + "24.0 mètres" + ) + time = 8.0 * ureg.second + self.assertEqual( + time.format_babel(locale='fr_FR', length='long'), + "8.0 secondes" + ) + self.assertEqual( + time.format_babel(locale='ro', length='short'), + "8.0 s" + ) + acceleration = distance / time ** 2 + self.assertEqual( + acceleration.format_babel(locale='fr_FR', length='long'), + "0.375 mètre par seconde²" + ) + mks = ureg.get_system('mks') + self.assertEqual( + mks.format_babel(locale='fr_FR'), + "métrique" + ) diff --git a/pint/unit.py b/pint/unit.py index edbede1..bde2212 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -95,6 +95,21 @@ class _Unit(SharedRegistryObject): return '%s' % (format(units, spec)) + def format_babel(self, spec='', **kwspec): + spec = spec or self.default_format + + if '~' in spec: + if self.dimensionless: + return '' + units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), + value) + for key, value in self._units.items())) + spec = spec.replace('~', '') + else: + units = self._units + + return '%s' % (units.format_babel(spec, **kwspec)) + # IPython related code def _repr_html_(self): return self.__format__('H') diff --git a/pint/util.py b/pint/util.py index 63c0860..8c2cb33 100644 --- a/pint/util.py +++ b/pint/util.py @@ -327,6 +327,9 @@ class UnitsContainer(Mapping): def __format__(self, spec): return format_unit(self, spec) + def format_babel(self, spec, **kwspec): + return format_unit(self, spec, **kwspec) + def __copy__(self): return UnitsContainer(self._d) diff --git a/pint/xtranslated.txt b/pint/xtranslated.txt new file mode 100644 index 0000000..3ccfd9d --- /dev/null +++ b/pint/xtranslated.txt @@ -0,0 +1,26 @@ + +# a few unit definitions added to use the translations by unicode cldr + +dietary_calorie = 1000 * calorie = Calorie +metric_cup = liter / 4 +mps = meter / second +square_inch = inch ** 2 = sq_in +square_mile = mile ** 2 = sq_mile +square_meter = kilometer ** 2 = sq_m +square_kilometer = kilometer ** 2 = sq_km +mile_scandinavian = 10000 * meter +century = 100 * year +cubic_mile = 1 * mile ** 3 = cu_mile = cubic_miles +cubic_yard = 1 * yard ** 3 = cu_yd = cubic_yards +cubic_foot = 1 * foot ** 3 = cu_ft = cubic_feet +cubic_inch = 1 * inch ** 3 = cu_in = cubic_inches +cubic_meter = 1 * meter ** 3 = cu_m +cubic_kilometer = 1 * kilometer ** 3 = cu_km +karat = [purity] = Karat + +[consumption] = [volume] / [length] +liter_per_kilometer = liter / kilometer +liter_per_100kilometers = liter / (100 * kilometers) + +[US_consumption] = [length] / [volume] +MPG = mile / gallon