# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Utilities shared by the different formats. """ import warnings from fractions import Fraction from astropy.utils.misc import did_you_mean from ..utils import maybe_simple_fraction def get_grouped_by_powers(bases, powers): """ Groups the powers and bases in the given `~astropy.units.CompositeUnit` into positive powers and negative powers for easy display on either side of a solidus. Parameters ---------- bases : list of `astropy.units.UnitBase` instances powers : list of int Returns ------- positives, negatives : tuple of lists Each element in each list is tuple of the form (*base*, *power*). The negatives have the sign of their power reversed (i.e. the powers are all positive). """ positive = [] negative = [] for base, power in zip(bases, powers): if power < 0: negative.append((base, -power)) elif power > 0: positive.append((base, power)) else: raise ValueError("Unit with 0 power") return positive, negative def split_mantissa_exponent(v, format_spec=".8g"): """ Given a number, split it into its mantissa and base 10 exponent parts, each as strings. If the exponent is too small, it may be returned as the empty string. Parameters ---------- v : float format_spec : str, optional Number representation formatting string Returns ------- mantissa, exponent : tuple of strings """ x = format(v, format_spec).split('e') if x[0] != '1.' + '0' * (len(x[0]) - 2): m = x[0] else: m = '' if len(x) == 2: ex = x[1].lstrip("0+") if len(ex) > 0 and ex[0] == '-': ex = '-' + ex[1:].lstrip('0') else: ex = '' return m, ex def decompose_to_known_units(unit, func): """ Partially decomposes a unit so it is only composed of units that are "known" to a given format. Parameters ---------- unit : `~astropy.units.UnitBase` instance func : callable This function will be called to determine if a given unit is "known". If the unit is not known, this function should raise a `ValueError`. Returns ------- unit : `~astropy.units.UnitBase` instance A flattened unit. """ from astropy.units import core if isinstance(unit, core.CompositeUnit): new_unit = core.Unit(unit.scale) for base, power in zip(unit.bases, unit.powers): new_unit = new_unit * decompose_to_known_units(base, func) ** power return new_unit elif isinstance(unit, core.NamedUnit): try: func(unit) except ValueError: if isinstance(unit, core.Unit): return decompose_to_known_units(unit._represents, func) raise return unit else: raise TypeError("unit argument must be a 'NamedUnit' or 'CompositeUnit', " f"not {type(unit)}") def format_power(power): """ Converts a value for a power (which may be floating point or a `fractions.Fraction` object), into a string looking like either an integer or a fraction, if the power is close to that. """ if not hasattr(power, 'denominator'): power = maybe_simple_fraction(power) if getattr(power, 'denonimator', None) == 1: power = power.nominator return str(power) def _try_decomposed(unit, format_decomposed): represents = getattr(unit, '_represents', None) if represents is not None: try: represents_string = format_decomposed(represents) except ValueError: pass else: return represents_string decomposed = unit.decompose() if decomposed is not unit: try: decompose_string = format_decomposed(decomposed) except ValueError: pass else: return decompose_string return None def did_you_mean_units(s, all_units, deprecated_units, format_decomposed): """ A wrapper around `astropy.utils.misc.did_you_mean` that deals with the display of deprecated units. Parameters ---------- s : str The invalid unit string all_units : dict A mapping from valid unit names to unit objects. deprecated_units : sequence The deprecated unit names format_decomposed : callable A function to turn a decomposed version of the unit into a string. Should return `None` if not possible Returns ------- msg : str A string message with a list of alternatives, or the empty string. """ def fix_deprecated(x): if x in deprecated_units: results = [x + ' (deprecated)'] decomposed = _try_decomposed( all_units[x], format_decomposed) if decomposed is not None: results.append(decomposed) return results return (x,) return did_you_mean(s, all_units, fix=fix_deprecated) def unit_deprecation_warning(s, unit, standard_name, format_decomposed): """ Raises a UnitsWarning about a deprecated unit in a given format. Suggests a decomposed alternative if one is available. Parameters ---------- s : str The deprecated unit name. unit : astropy.units.core.UnitBase The unit object. standard_name : str The name of the format for which the unit is deprecated. format_decomposed : callable A function to turn a decomposed version of the unit into a string. Should return `None` if not possible """ from astropy.units.core import UnitsWarning message = f"The unit '{s}' has been deprecated in the {standard_name} standard." decomposed = _try_decomposed(unit, format_decomposed) if decomposed is not None: message += f" Suggested: {decomposed}." warnings.warn(message, UnitsWarning)