# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Handles a "generic" string format for units
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from .base import Base
from . import utils
from ...utils.compat.fractions import Fraction
[docs]class Generic(Base):
"""
A "generic" format.
The syntax of the format is based directly on the FITS standard,
but instead of only supporting the units that FITS knows about, it
supports any unit available in the `astropy.units` namespace.
"""
_show_scale = True
def __init__(self):
# Build this on the class, so it only gets generated once.
if '_parser' not in Generic.__dict__:
Generic._parser = self._make_parser()
@classmethod
def _make_parser(cls):
"""
The grammar here is based on the description in the `FITS
standard
<http://fits.gsfc.nasa.gov/standard30/fits_standard30aa.pdf>`_,
Section 4.3, which is not terribly precise. The exact grammar
is here is based on the YACC grammar in the `unity library
<https://bitbucket.org/nxg/unity/>`_.
This same grammar is used by the `"fits"` and `"vounit"`
formats, the only difference being the set of available unit
strings.
"""
from astropy.extern import pyparsing as p
product = p.Literal("*") | p.Literal(".") | p.White()
division = p.Literal("/")
power = p.Literal("**") | p.Literal("^") | p.Empty()
open_p = p.Literal("(")
close_p = p.Literal(")")
# TODO: We only support the sqrt function for now because it's
# obvious how to handle it.
function_name = p.Literal("sqrt")
unsigned_integer = p.Regex(r'\d+')
signed_integer = p.Regex(r'[+-]\d+')
integer = p.Regex(r'[+-]?\d+')
floating_point = p.Regex(r'[+-]?((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+)?')
division_product_of_units = p.Forward()
factor = p.Forward()
factor_product_of_units = p.Forward()
frac = p.Forward()
function = p.Forward()
main = p.Forward()
numeric_power = p.Forward()
product_of_units = p.Forward()
unit_expression = p.Forward()
unit = p.Forward()
unit_with_power = p.Forward()
main << (
(factor_product_of_units) ^
(division_product_of_units))
factor_product_of_units << (
(p.Optional(factor +
p.Suppress(p.Optional(p.White())), default=1.0) +
product_of_units +
p.StringEnd()))
division_product_of_units << (
(p.Optional(factor, default=1.0) +
p.Optional(product_of_units, default=1.0) +
p.Suppress(division) +
p.Suppress(p.ZeroOrMore(p.White())) +
unit_expression +
p.StringEnd()))
product_of_units << (
(unit_expression + p.Suppress(product) + product_of_units) ^
(unit_expression))
function << (
function_name +
p.Suppress(open_p) + unit_expression + p.Suppress(close_p))
unit_expression << (
(function) ^
(unit_with_power) ^
(p.Suppress(open_p) + product_of_units + p.Suppress(close_p))
)
factor << (
(unsigned_integer + signed_integer) ^
(unsigned_integer + p.Suppress(power) + numeric_power) ^
(floating_point + p.Suppress(p.White()) +
unsigned_integer + signed_integer) ^
(floating_point + p.Suppress(p.White()) +
unsigned_integer + p.Suppress(power) + numeric_power) ^
(floating_point)
)
unit << p.Word(p.alphas, p.alphas + '_')
unit_with_power << (
(unit + p.Suppress(power) + numeric_power) ^
(unit))
numeric_power << (
integer |
(p.Suppress(open_p) + integer + p.Suppress(close_p)) ^
(p.Suppress(open_p) + floating_point + p.Suppress(close_p)) ^
(p.Suppress(open_p) + frac + p.Suppress(close_p)))
frac << (
integer + p.Suppress(division) + integer)
# Set actions
for key, val in locals().items():
if isinstance(val, p.ParserElement):
val.setName(key)
val.leaveWhitespace()
method_name = "_parse_{0}".format(key)
if hasattr(cls, method_name):
val.setParseAction(getattr(cls, method_name))
return main
@classmethod
@utils._trace
def _parse_unsigned_integer(cls, s, loc, toks):
return int(toks[0])
@classmethod
@utils._trace
def _parse_signed_integer(cls, s, loc, toks):
return int(toks[0])
@classmethod
@utils._trace
def _parse_integer(cls, s, loc, toks):
return int(toks[0])
@classmethod
@utils._trace
def _parse_floating_point(cls, s, loc, toks):
return float(toks[0])
@classmethod
@utils._trace
def _parse_factor(cls, s, loc, toks):
if len(toks) == 1:
return toks[0]
elif len(toks) == 2:
return toks[0] ** float(toks[1])
elif len(toks) == 3:
return float(toks[0]) * toks[1] ** float(toks[2])
@classmethod
@utils._trace
def _parse_frac(cls, s, loc, toks):
return Fraction(toks[0], toks[1])
@classmethod
@utils._trace
def _parse_unit(cls, s, loc, toks):
from astropy.extern import pyparsing as p
if toks[0] in cls._unit_namespace:
return cls._unit_namespace[toks[0]]
raise p.ParseException(
s, loc, "{0!r} is not a recognized unit".format(toks[0]))
@classmethod
@utils._trace
def _parse_product_of_units(cls, s, loc, toks):
if len(toks) == 1:
return toks[0]
else:
return toks[0] * toks[1]
@classmethod
@utils._trace
def _parse_division_product_of_units(cls, s, loc, toks):
from ..core import Unit
return Unit((toks[0] * toks[1]) / toks[2])
@classmethod
@utils._trace
def _parse_factor_product_of_units(cls, s, loc, toks):
if toks[0] != 1.0:
from ..core import Unit
return Unit(toks[0] * toks[1])
else:
return toks[1]
@classmethod
@utils._trace
def _parse_unit_with_power(cls, s, loc, toks):
if len(toks) == 1:
return toks[0]
else:
return toks[0] ** toks[1]
@classmethod
@utils._trace
def _parse_function(cls, s, loc, toks):
from astropy.extern import pyparsing as p
# TODO: Add support for more functions here
if toks[0] == 'sqrt':
return toks[1] ** -2.0
else:
raise p.ParseException(
s, loc, "{0!r} is not a recognized function".format(
toks[0]))
[docs] def parse(self, s):
from astropy.extern import pyparsing as p
if utils.DEBUG:
print("parse", s)
if '_unit_namespace' not in Generic.__dict__:
from ... import units as u
ns = {}
for key, val in u.__dict__.items():
if isinstance(val, u.UnitBase):
ns[key] = val
Generic._unit_namespace = ns
# This is a short circuit for the case where the string
# is just a single unit name
try:
return self._parse_unit(s, 0, [s])
except p.ParseException as e:
try:
return self._parser.parseString(s, parseAll=True)[0]
except p.ParseException as e:
raise ValueError("{0} in {1!r}".format(
utils.cleanup_pyparsing_error(e), s))
def _get_unit_name(self, unit):
return unit.get_format_name('generic')
def _format_unit_list(self, units):
out = []
units.sort(key=lambda x: self._get_unit_name(x[0]).lower())
for base, power in units:
if power == 1:
out.append(self._get_unit_name(base))
else:
if not isinstance(power, Fraction):
if power % 1.0 != 0.0:
power = Fraction(power).limit_denominator(10)
if power.denominator == 1:
power = int(power.numerator)
else:
power = int(power)
if isinstance(power, Fraction):
out.append('{0}({1})'.format(
self._get_unit_name(base), power))
else:
out.append('{0}{1}'.format(
self._get_unit_name(base), power))
return ' '.join(out)
[docs] def to_string(self, unit):
from .. import core
if isinstance(unit, core.CompositeUnit):
if unit.scale != 1 and self._show_scale:
s = '{0:e} '.format(unit.scale)
else:
s = ''
if len(unit.bases):
positives, negatives = utils.get_grouped_by_powers(
unit.bases, unit.powers)
if len(positives):
s += self._format_unit_list(positives)
elif s == '':
s = '1'
if len(negatives):
s += ' / ({0})'.format(self._format_unit_list(negatives))
elif isinstance(unit, core.NamedUnit):
s = self._get_unit_name(unit)
return s
[docs]class Unscaled(Generic):
"""
A format that doesn't display the scale part of the unit, other
than that, it is identical to the `Generic` format.
This is used in some error messages where the scale is irrelevant.
"""
_show_scale = False