From dcb3df3ce2115ecbef09607e3e5955e341d79871 Mon Sep 17 00:00:00 2001 From: "C.D. Clark III" Date: Mon, 7 Dec 2015 16:51:32 -0600 Subject: [PATCH 1/2] Added support for siunitx formatting. Added support for generating siunitx commands in the __format__ functions for Quantity, Measurment, and Unit classes. This adds a new code, 'Lx', to the formatting spec, that if present will trigger the LaTeX siunitx formatting. All other formatting options in the spec string are be passed through. Currently does not accept siunit options. --- pint/formatting.py | 37 ++++++++++++++++++++++++++++++ pint/measurement.py | 18 ++++++++++++++- pint/quantity.py | 14 ++++++++++- pint/testsuite/test_measurement.py | 4 ++++ pint/testsuite/test_quantity.py | 1 + pint/testsuite/test_unit.py | 1 + pint/unit.py | 8 +++++++ pint/util.py | 2 +- 8 files changed, 82 insertions(+), 3 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index 2a70710..8d7f8e6 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -190,6 +190,43 @@ def format_unit(unit, spec): return result +def siunitx_format_unit(units): + '''Returns LaTeX code for the unit that can be put into an siunitx command.''' + # NOTE: unit registry is required to identify unit prefixes. + registry = units._REGISTRY + + def _tothe(power): + if power == 1: + return '' + elif power == 2: + return r'\squared' + elif power == 3: + return r'\cubed' + else: + return r'\tothe{%d}' % (power) + + l = [] + # loop through all units in the container + for unit, power in sorted(units._units.items()): + # remove unit prefix if it exists + # siunit supports \prefix commands + prefix = None + for p in registry._prefixes.values(): + p = str(p) + if len(p) > 0 and unit.find(p) == 0: + prefix = p + unit = unit.replace( prefix, '', 1 ) + + if power < 0: + l.append(r'\per') + if not prefix is None: + l.append(r'\{0}'.format(prefix)) + l.append(r'\{0}'.format(unit)) + l.append(r'{0}'.format(_tothe(abs(power)))) + + return ''.join(l) + + def remove_custom_flags(spec): for flag in _KNOWN_TYPES: if flag: diff --git a/pint/measurement.py b/pint/measurement.py index e82591d..d0d239f 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -10,7 +10,7 @@ from __future__ import division, unicode_literals, print_function, absolute_import from .compat import ufloat -from .formatting import _FORMATS +from .formatting import _FORMATS, siunitx_format_unit MISSING = object() @@ -65,6 +65,22 @@ class _Measurement(object): return '{0}'.format(self) def __format__(self, spec): + # special cases + if 'Lx' in spec: # the LaTeX siunitx code + # the uncertainties module supports formatting + # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), + # which siunitx actually accepts as input. we just need to give the 'S' + # formatting option for the uncertainties module. + spec = spec.replace('Lx','S') + # todo: add support for extracting options + opts = 'separate-uncertainty=true' + mstr = format( self.magnitude, spec ) + ustr = siunitx_format_unit(self.units) + ret = r'\SI[%s]{%s}{%s}'%( opts, mstr, ustr ) + return ret + + + # standard cases if 'L' in spec: newpm = pm = r' \pm ' pars = _FORMATS['L']['parentheses_fmt'] diff --git a/pint/quantity.py b/pint/quantity.py index 76e8f4f..b7c8bd8 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -15,7 +15,7 @@ import operator import functools import bisect -from .formatting import remove_custom_flags +from .formatting import remove_custom_flags, siunitx_format_unit from .errors import (DimensionalityError, OffsetUnitCalculusError, UndefinedUnitError) from .definitions import UnitDefinition @@ -118,6 +118,18 @@ class _Quantity(SharedRegistryObject): def __format__(self, spec): spec = spec or self.default_format + + # special cases + if 'Lx' in spec: # the LaTeX siunitx code + spec = spec.replace('Lx','') + # todo: add support for extracting options + opts = '' + mstr = format(self.magnitude,spec) + ustr = siunitx_format_unit(self.units) + ret = r'\SI[%s]{%s}{%s}'%( opts, mstr, ustr ) + return ret + + # standard cases if '#' in spec: spec = spec.replace('#', '') obj = self.to_compact() diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index bf1487c..b7f2a50 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -51,11 +51,13 @@ class TestMeasurement(QuantityTestCase): self.assertEqual('{0:L}'.format(m), r'\left(4.00 \pm 0.10\right) second^{2}') self.assertEqual('{0:H}'.format(m), '(4.00 ± 0.10) second2') self.assertEqual('{0:C}'.format(m), '(4.00+/-0.10) second**2') + self.assertEqual('{0:Lx}'.format(m), r'\SI[separate-uncertainty=true]{4.00(10)}{\second\squared}') self.assertEqual('{0:.1f}'.format(m), '(4.0 +/- 0.1) second ** 2') self.assertEqual('{0:.1fP}'.format(m), '(4.0 ± 0.1) second²') self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right) second^{2}') self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second2') self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') + self.assertEqual('{0:.1fLx}'.format(m), '\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') def test_format_paru(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') @@ -75,6 +77,8 @@ class TestMeasurement(QuantityTestCase): self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right) second^{2}') self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second2') self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') + self.assertEqual('{0:.3uLx}'.format(m), '\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') + self.assertEqual('{0:.1uLx}'.format(m), '\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') def test_format_percu(self): self.test_format_perce() diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index d701364..3ed98a2 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -104,6 +104,7 @@ class TestQuantity(QuantityTestCase): ('{0:P~}', '4.12345678 kg·m²/s'), ('{0:H~}', '4.12345678 kg m2/s'), ('{0:C~}', '4.12345678 kg*m**2/s'), + ('{0:Lx}', r'\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}'), ): self.assertEqual(spec.format(x), result) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index edbb2fa..d170c90 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -32,6 +32,7 @@ class TestUnit(QuantityTestCase): ('{0:P}', 'kilogram·meter²/second'), ('{0:H}', 'kilogram meter2/second'), ('{0:C}', 'kilogram*meter**2/second'), + ('{0:Lx}', r'\si[]{\kilo\gram\meter\squared\per\second}'), ('{0:~}', 'kg * m ** 2 / s'), ('{0:L~}', r'\frac{kg \cdot m^{2}}{s}'), ('{0:P~}', 'kg·m²/s'), diff --git a/pint/unit.py b/pint/unit.py index 1aa7323..5de2257 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -30,6 +30,7 @@ from .util import (logger, pi_theorem, solve_dependencies, ParserHelper, find_shortest_path, UnitsContainer, _is_dim, SharedRegistryObject, to_units_container) from .compat import tokenizer, string_types, NUMERIC_TYPES, long_type, zip_longest +from .formatting import siunitx_format_unit from .definitions import (Definition, UnitDefinition, PrefixDefinition, DimensionDefinition) from .converters import ScaleConverter @@ -99,6 +100,13 @@ class _Unit(SharedRegistryObject): def __format__(self, spec): spec = spec or self.default_format + # special cases + if 'Lx' in spec: # the LaTeX siunitx code + opts = '' + ustr = siunitx_format_unit(self) + ret = r'\si[%s]{%s}'%( opts, ustr ) + return ret + if '~' in spec: units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), diff --git a/pint/util.py b/pint/util.py index 31e8206..d0bd14f 100644 --- a/pint/util.py +++ b/pint/util.py @@ -22,7 +22,7 @@ from token import STRING, NAME, OP, NUMBER from tokenize import untokenize from .compat import string_types, tokenizer, lru_cache, NullHandler, maketrans, NUMERIC_TYPES -from .formatting import format_unit +from .formatting import format_unit,siunitx_format_unit from .pint_eval import build_eval_tree logger = logging.getLogger(__name__) From 428d1d2c025bdf533ff68ea0f3aed4bf15578c22 Mon Sep 17 00:00:00 2001 From: "C.D. Clark III" Date: Mon, 7 Dec 2015 17:54:05 -0600 Subject: [PATCH 2/2] Updated documentation for siunitx support. Added siunitx example to the tutorial in the documentation. --- docs/tutorial.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index dd345e7..7da7422 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -248,6 +248,15 @@ If you want to use abbreviated unit names, prefix the specification with `~`: The same is true for latex (`L`) and HTML (`H`) specs. +Pint also supports the LaTeX siunitx package: + +.. doctest:: + + >>> accel = 1.3 * ureg['meter/second**2'] + >>> # siunitx Latex print + >>> print('The siunitx representation is {:Lx}'.format(accel)) + The siunitx representation is \SI[]{1.3}{\meter\per\second\squared} + Finally, you can specify a default format specification: >>> 'The acceleration is {}'.format(accel)