# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICNSE.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
# TODO: Support logarithmic units using bracketed syntax
[docs]class CDS(Base):
"""
Support the `Centre de Données astronomiques de Strasbourg
<http://cds.u-strasbg.fr/>`_ `Standards for Astronomical
Catalogues 2.0 <http://cds.u-strasbg.fr/doc/catstd-3.2.htx>`_
format. This format is used by VOTable up to version 1.2.
"""
def __init__(self):
# Build this on the class, so it only gets generated once.
if not '_units' in CDS.__dict__:
CDS._units = self._generate_unit_names()
if '_parser' not in CDS.__dict__:
CDS._parser = self._make_parser()
@staticmethod
def _generate_unit_names():
from ... import units as u
names = {}
names['%'] = u.Unit(0.01)
bases = [
'A', 'C', 'cd', 'eV', 'F', 'g', 'H', 'Hz', 'J', 'K',
'lm', 'lx', 'm', 'mol', 'N', 'Ohm', 'Pa', 'rad', 's', 'S',
'sr', 'T', 'V', 'W', 'Wb']
prefixes = [
'y', 'z', 'a', 'f', 'p', 'n', 'u', 'm', 'c', 'd',
'', 'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
for base in bases:
for prefix in prefixes:
key = prefix + base
names[key] = getattr(u, key)
simple_bases = [
'a', 'AU', 'arcmin', 'arcsec', 'barn', 'bit',
'byte', 'ct', 'D', 'd', 'deg', 'h', 'Jy', 'mag', 'mas',
'min', 'pc', 'pix', 'Ry', 'solLum', 'solMass', 'solRad',
'Sun', 'yr']
for base in simple_bases:
names[base] = getattr(u, base)
return names
@classmethod
def _make_parser(cls):
"""
The grammar here is based on the description in the `Standards
for Astronomical Catalogues 2.0
<http://cds.u-strasbg.fr/doc/catstd-3.2.htx>`_, 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/>`_.
"""
from astropy.extern import pyparsing as p
product = p.Literal(".")
division = p.Literal("/")
open_p = p.Literal("(")
close_p = p.Literal(")")
literal10 = p.Literal("10")
literalx = p.Literal("x")
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+)?')
factor = 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 << (
(p.Optional(factor, default=1.0) +
product_of_units +
p.StringEnd()))
product_of_units << (
(unit_expression + p.StringEnd()) ^
(division + unit_expression) ^
(unit_expression + product + product_of_units) ^
(unit_expression + division + product_of_units))
unit_expression << (
(unit_with_power) ^
(p.Suppress(open_p) + product_of_units + p.Suppress(close_p)))
factor << (
(floating_point + literalx + literal10 + signed_integer) ^
(literal10 + signed_integer) ^
(unsigned_integer) ^
(literal10) ^
(floating_point))
unit_with_power << (
(unit + p.Optional(numeric_power)))
numeric_power << (
integer)
unit << (
p.Literal('%') |
p.Word(p.alphas, p.alphas + '_'))
# 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 10.0 ** float(toks[1])
elif len(toks) == 4:
return toks[0] * 10.0 ** toks[3]
@classmethod
@utils._trace
def _parse_unit_with_power(cls, s, loc, toks):
if len(toks) == 1:
return toks[0]
elif len(toks) == 2:
return toks[0] ** toks[1]
@classmethod
@utils._trace
def _parse_unit(cls, s, loc, toks):
from astropy.extern import pyparsing as p
unit = toks[0]
if unit not in cls._units:
raise p.ParseException(
"Unit {0!r} not supported by the CDS SAC "
"standard.".format(unit))
return cls._units[unit]
@classmethod
@utils._trace
def _parse_product_of_units(cls, s, loc, toks):
if len(toks) == 1:
return toks[0]
elif len(toks) == 2:
from ..core import Unit
return Unit(1.0 / toks[1])
elif len(toks) == 3:
if toks[1] == '/':
return toks[0] / toks[2]
else:
return toks[0] * toks[2]
@classmethod
@utils._trace
def _parse_main(cls, s, loc, toks):
from ..core import Unit
return Unit(toks[0] * toks[1])
[docs] def parse(self, s):
from astropy.extern import pyparsing as p
if utils.DEBUG:
print("parse", s)
if ' ' in s:
raise ValueError('CDS unit must not contain whitespace')
# 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('cds')
def _format_unit_list(self, units):
out = []
for base, power in units:
if power == 1:
out.append(self._get_unit_name(base))
else:
out.append('{0}{1}'.format(
self._get_unit_name(base), power))
return '.'.join(out)
[docs] def to_string(self, unit):
from .. import core
# Remove units that aren't known to the format
unit = utils.decompose_to_known_units(unit, self._get_unit_name)
if isinstance(unit, core.CompositeUnit):
s = ''
if unit.scale != 1:
m, e = utils.split_mantissa_exponent(unit.scale)
parts = []
if m:
parts.append(m)
if e:
if not e.startswith('-'):
e = "+" + e
parts.append('10{0}'.format(e))
s = 'x'.join(parts)
else:
s = ''
pairs = zip(unit.bases, unit.powers)
pairs.sort(key=lambda x: x[1], reverse=True)
s += self._format_unit_list(pairs)
elif isinstance(unit, core.NamedUnit):
s = self._get_unit_name(unit)
return s