Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
units.py344 linesDownload Raw Back to babel
1from __future__ import annotations2 3import decimal4from typing import TYPE_CHECKING5 6from babel.core import Locale7from babel.numbers import LC_NUMERIC, format_decimal8 9if TYPE_CHECKING:10    from typing_extensions import Literal11 12 13class UnknownUnitError(ValueError):14    def __init__(self, unit: str, locale: Locale) -> None:15        ValueError.__init__(self, f"{unit} is not a known unit in {locale}")16 17 18def get_unit_name(19    measurement_unit: str,20    length: Literal['short', 'long', 'narrow'] = 'long',21    locale: Locale | str | None = LC_NUMERIC,22) -> str | None:23    """24    Get the display name for a measurement unit in the given locale.25 26    >>> get_unit_name("radian", locale="en")27    'radians'28 29    Unknown units will raise exceptions:30 31    >>> get_unit_name("battery", locale="fi")32    Traceback (most recent call last):33        ...34    UnknownUnitError: battery/long is not a known unit/length in fi35 36    :param measurement_unit: the code of a measurement unit.37                             Known units can be found in the CLDR Unit Validity XML file:38                             https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml39 40    :param length: "short", "long" or "narrow"41    :param locale: the `Locale` object or locale identifier42    :return: The unit display name, or None.43    """44    locale = Locale.parse(locale)45    unit = _find_unit_pattern(measurement_unit, locale=locale)46    if not unit:47        raise UnknownUnitError(unit=measurement_unit, locale=locale)48    return locale.unit_display_names.get(unit, {}).get(length)49 50 51def _find_unit_pattern(unit_id: str, locale: Locale | str | None = LC_NUMERIC) -> str | None:52    """53    Expand a unit into a qualified form.54 55    Known units can be found in the CLDR Unit Validity XML file:56    https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml57 58    >>> _find_unit_pattern("radian", locale="en")59    'angle-radian'60 61    Unknown values will return None.62 63    >>> _find_unit_pattern("horse", locale="en")64 65    :param unit_id: the code of a measurement unit.66    :return: A key to the `unit_patterns` mapping, or None.67    """68    locale = Locale.parse(locale)69    unit_patterns = locale._data["unit_patterns"]70    if unit_id in unit_patterns:71        return unit_id72    for unit_pattern in sorted(unit_patterns, key=len):73        if unit_pattern.endswith(unit_id):74            return unit_pattern75    return None76 77 78def format_unit(79    value: str | float | decimal.Decimal,80    measurement_unit: str,81    length: Literal['short', 'long', 'narrow'] = 'long',82    format: str | None = None,83    locale: Locale | str | None = LC_NUMERIC,84    *,85    numbering_system: Literal["default"] | str = "latn",86) -> str:87    """Format a value of a given unit.88 89    Values are formatted according to the locale's usual pluralization rules90    and number formats.91 92    >>> format_unit(12, 'length-meter', locale='ro_RO')93    u'12 metri'94    >>> format_unit(15.5, 'length-mile', locale='fi_FI')95    u'15,5 mailia'96    >>> format_unit(1200, 'pressure-millimeter-ofhg', locale='nb')97    u'1\\xa0200 millimeter kvikks\\xf8lv'98    >>> format_unit(270, 'ton', locale='en')99    u'270 tons'100    >>> format_unit(1234.5, 'kilogram', locale='ar_EG', numbering_system='default')101    u'1٬234٫5 كيلوغرام'102 103    Number formats may be overridden with the ``format`` parameter.104 105    >>> import decimal106    >>> format_unit(decimal.Decimal("-42.774"), 'temperature-celsius', 'short', format='#.0', locale='fr')107    u'-42,8\\u202f\\xb0C'108 109    The locale's usual pluralization rules are respected.110 111    >>> format_unit(1, 'length-meter', locale='ro_RO')112    u'1 metru'113    >>> format_unit(0, 'length-mile', locale='cy')114    u'0 mi'115    >>> format_unit(1, 'length-mile', locale='cy')116    u'1 filltir'117    >>> format_unit(3, 'length-mile', locale='cy')118    u'3 milltir'119 120    >>> format_unit(15, 'length-horse', locale='fi')121    Traceback (most recent call last):122        ...123    UnknownUnitError: length-horse is not a known unit in fi124 125    .. versionadded:: 2.2.0126 127    :param value: the value to format. If this is a string, no number formatting will be attempted.128    :param measurement_unit: the code of a measurement unit.129                             Known units can be found in the CLDR Unit Validity XML file:130                             https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml131    :param length: "short", "long" or "narrow"132    :param format: An optional format, as accepted by `format_decimal`.133    :param locale: the `Locale` object or locale identifier134    :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".135                             The special value "default" will use the default numbering system of the locale.136    :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.137    """138    locale = Locale.parse(locale)139 140    q_unit = _find_unit_pattern(measurement_unit, locale=locale)141    if not q_unit:142        raise UnknownUnitError(unit=measurement_unit, locale=locale)143    unit_patterns = locale._data["unit_patterns"][q_unit].get(length, {})144 145    if isinstance(value, str):  # Assume the value is a preformatted singular.146        formatted_value = value147        plural_form = "one"148    else:149        formatted_value = format_decimal(value, format, locale, numbering_system=numbering_system)150        plural_form = locale.plural_form(value)151 152    if plural_form in unit_patterns:153        return unit_patterns[plural_form].format(formatted_value)154 155    # Fall back to a somewhat bad representation.156    # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen.157    fallback_name = get_unit_name(measurement_unit, length=length, locale=locale)  # pragma: no cover158    return f"{formatted_value} {fallback_name or measurement_unit}"  # pragma: no cover159 160 161def _find_compound_unit(162    numerator_unit: str,163    denominator_unit: str,164    locale: Locale | str | None = LC_NUMERIC,165) -> str | None:166    """167    Find a predefined compound unit pattern.168 169    Used internally by format_compound_unit.170 171    >>> _find_compound_unit("kilometer", "hour", locale="en")172    'speed-kilometer-per-hour'173 174    >>> _find_compound_unit("mile", "gallon", locale="en")175    'consumption-mile-per-gallon'176 177    If no predefined compound pattern can be found, `None` is returned.178 179    >>> _find_compound_unit("gallon", "mile", locale="en")180 181    >>> _find_compound_unit("horse", "purple", locale="en")182 183    :param numerator_unit: The numerator unit's identifier184    :param denominator_unit: The denominator unit's identifier185    :param locale: the `Locale` object or locale identifier186    :return: A key to the `unit_patterns` mapping, or None.187    :rtype: str|None188    """189    locale = Locale.parse(locale)190 191    # Qualify the numerator and denominator units.  This will turn possibly partial192    # units like "kilometer" or "hour" into actual units like "length-kilometer" and193    # "duration-hour".194 195    resolved_numerator_unit = _find_unit_pattern(numerator_unit, locale=locale)196    resolved_denominator_unit = _find_unit_pattern(denominator_unit, locale=locale)197 198    # If either was not found, we can't possibly build a suitable compound unit either.199    if not (resolved_numerator_unit and resolved_denominator_unit):200        return None201 202    # Since compound units are named "speed-kilometer-per-hour", we'll have to slice off203    # the quantities (i.e. "length", "duration") from both qualified units.204 205    bare_numerator_unit = resolved_numerator_unit.split("-", 1)[-1]206    bare_denominator_unit = resolved_denominator_unit.split("-", 1)[-1]207 208    # Now we can try and rebuild a compound unit specifier, then qualify it:209 210    return _find_unit_pattern(f"{bare_numerator_unit}-per-{bare_denominator_unit}", locale=locale)211 212 213def format_compound_unit(214    numerator_value: str | float | decimal.Decimal,215    numerator_unit: str | None = None,216    denominator_value: str | float | decimal.Decimal = 1,217    denominator_unit: str | None = None,218    length: Literal["short", "long", "narrow"] = "long",219    format: str | None = None,220    locale: Locale | str | None = LC_NUMERIC,221    *,222    numbering_system: Literal["default"] | str = "latn",223) -> str | None:224    """225    Format a compound number value, i.e. "kilometers per hour" or similar.226 227    Both unit specifiers are optional to allow for formatting of arbitrary values still according228    to the locale's general "per" formatting specifier.229 230    >>> format_compound_unit(7, denominator_value=11, length="short", locale="pt")231    '7/11'232 233    >>> format_compound_unit(150, "kilometer", denominator_unit="hour", locale="sv")234    '150 kilometer per timme'235 236    >>> format_compound_unit(150, "kilowatt", denominator_unit="year", locale="fi")237    '150 kilowattia / vuosi'238 239    >>> format_compound_unit(32.5, "ton", 15, denominator_unit="hour", locale="en")240    '32.5 tons per 15 hours'241 242    >>> format_compound_unit(1234.5, "ton", 15, denominator_unit="hour", locale="ar_EG", numbering_system="arab")243    '1٬234٫5 طن لكل 15 ساعة'244 245    >>> format_compound_unit(160, denominator_unit="square-meter", locale="fr")246    '160 par m\\xe8tre carr\\xe9'247 248    >>> format_compound_unit(4, "meter", "ratakisko", length="short", locale="fi")249    '4 m/ratakisko'250 251    >>> format_compound_unit(35, "minute", denominator_unit="nautical-mile", locale="sv")252    '35 minuter per nautisk mil'253 254    >>> from babel.numbers import format_currency255    >>> format_compound_unit(format_currency(35, "JPY", locale="de"), denominator_unit="liter", locale="de")256    '35\\xa0\\xa5 pro Liter'257 258    See https://www.unicode.org/reports/tr35/tr35-general.html#perUnitPatterns259 260    :param numerator_value: The numerator value. This may be a string,261                            in which case it is considered preformatted and the unit is ignored.262    :param numerator_unit: The numerator unit. See `format_unit`.263    :param denominator_value: The denominator value. This may be a string,264                              in which case it is considered preformatted and the unit is ignored.265    :param denominator_unit: The denominator unit. See `format_unit`.266    :param length: The formatting length. "short", "long" or "narrow"267    :param format: An optional format, as accepted by `format_decimal`.268    :param locale: the `Locale` object or locale identifier269    :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".270                             The special value "default" will use the default numbering system of the locale.271    :return: A formatted compound value.272    :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.273    """274    locale = Locale.parse(locale)275 276    # Look for a specific compound unit first...277 278    if numerator_unit and denominator_unit and denominator_value == 1:279        compound_unit = _find_compound_unit(numerator_unit, denominator_unit, locale=locale)280        if compound_unit:281            return format_unit(282                numerator_value,283                compound_unit,284                length=length,285                format=format,286                locale=locale,287                numbering_system=numbering_system,288            )289 290    # ... failing that, construct one "by hand".291 292    if isinstance(numerator_value, str):  # Numerator is preformatted293        formatted_numerator = numerator_value294    elif numerator_unit:  # Numerator has unit295        formatted_numerator = format_unit(296            numerator_value,297            numerator_unit,298            length=length,299            format=format,300            locale=locale,301            numbering_system=numbering_system,302        )303    else:  # Unitless numerator304        formatted_numerator = format_decimal(305            numerator_value,306            format=format,307            locale=locale,308            numbering_system=numbering_system,309        )310 311    if isinstance(denominator_value, str):  # Denominator is preformatted312        formatted_denominator = denominator_value313    elif denominator_unit:  # Denominator has unit314        if denominator_value == 1:  # support perUnitPatterns when the denominator is 1315            denominator_unit = _find_unit_pattern(denominator_unit, locale=locale)316            per_pattern = locale._data["unit_patterns"].get(denominator_unit, {}).get(length, {}).get("per")317            if per_pattern:318                return per_pattern.format(formatted_numerator)319            # See TR-35's per-unit pattern algorithm, point 3.2.320            # For denominator 1, we replace the value to be formatted with the empty string;321            # this will make `format_unit` return " second" instead of "1 second".322            denominator_value = ""323 324        formatted_denominator = format_unit(325            denominator_value,326            measurement_unit=(denominator_unit or ""),327            length=length,328            format=format,329            locale=locale,330            numbering_system=numbering_system,331        ).strip()332    else:  # Bare denominator333        formatted_denominator = format_decimal(334            denominator_value,335            format=format,336            locale=locale,337            numbering_system=numbering_system,338        )339 340    # TODO: this doesn't support "compound_variations" (or "prefix"), and will fall back to the "x/y" representation341    per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, {}).get("compound", "{0}/{1}")342 343    return per_pattern.format(formatted_numerator, formatted_denominator)344 
codekingpro/portable-devtools · Team Ai