diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0fea9d..458e590b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to this project after the 0.2.0 release will be documented i The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.12.0][] - 2025-01-05 + +Python package release, documentation update, continued addition of new units and other updates and fixes + +### Changed + +- Updated copyright dates for 2025 [#356][] +- Python getters `value()` and `commodity()`, `multiplier()`, and `units()` are now properties and do not use the parenthesis any more. (these are read only, the classes are immutable) [#357][] + +### Fixed + +- fixed some code analyzer warnings [#355][] + +### Added + +- Added currency conversion from around the world [#355][] +- Added commodity_conversion_map file containing some additional commoditity strings [#355][] +- In python library added dunder methods for floor, ceil, round, hash, floordiv [#357][] +- Added format specifiers for measurement to allow conversion in the format string and removal of the unit string [#357][] +- Added operators for `float` and `bool` in python [#357][] +- Added mod (`%`) and `//` operator in python for both other `Measurement` and `float` [#357][] +- Added negation operator `-` in python [#357][] + +### Removed + +- removed specific python method `inv` - now just use inversion operator `~` [#357][] +- remove isolated `to_string` method on Unit and Measurement python classes, use `str()` [#357][] + +[#355]: https://github.com/LLNL/units/pull/355 +[#356]: https://github.com/LLNL/units/pull/356 +[#357]: https://github.com/LLNL/units/pull/357 + ## [0.11.0][] -2024-12-26 Python package release, documentation update, continued addition of new units and other updates and fixes @@ -130,6 +162,7 @@ A few user suggested tweaks, and support additional unit string conversions supp [0.9.2]: https://github.com/LLNL/units/releases/tag/v0.9.2 [0.10.2]: https://github.com/LLNL/units/releases/tag/v0.10.2 [0.11.0]: https://github.com/LLNL/units/releases/tag/v0.11.0 +[0.12.0]: https://github.com/LLNL/units/releases/tag/v0.12.0 ## [0.7.0][] - 2022-12-17 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ebeba07..3002307c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ endif() project( ${UNITS_CMAKE_PROJECT_NAME} LANGUAGES C CXX - VERSION 0.11.0 + VERSION 0.12.0 ) include(CMakeDependentOption) include(CTest) diff --git a/README.md b/README.md index f1011286..0e219b18 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![codecov](https://codecov.io/gh/LLNL/units/branch/main/graph/badge.svg)](https://codecov.io/gh/LLNL/units) [![Build Status](https://dev.azure.com/phlptp/units/_apis/build/status/LLNL.units?branchName=main)](https://dev.azure.com/phlptp/units/_build/latest?definitionId=1&branchName=main) [![CircleCI](https://circleci.com/gh/LLNL/units.svg?style=svg)](https://circleci.com/gh/LLNL/units) +[![](https://img.shields.io/pypi/pyversions/units-llnl)](https://pypi.org/project/units-llnl/) +[![](https://img.shields.io/pypi/v/units-llnl)](https://pypi.org/project/units-llnl/) [![](https://img.shields.io/badge/License-BSD-blue.svg)](https://github.com/GMLC-TDC/HELICS-src/blob/main/LICENSE) [![Documentation Status](https://readthedocs.org/projects/units/badge/?version=latest)](https://units.readthedocs.io/en/latest/?badge=latest) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/LLNL/units/main.svg)](https://results.pre-commit.ci/latest/github/LLNL/units/main) @@ -13,7 +15,7 @@ The Units library provides a means of working with units of measurement at runtime, including conversion to and from strings. It provides a small number of types for working with units and measurements and operations necessary for user input and output with units. -This software was developed for use in [LLNL/GridDyn](https://github.com/LLNL/GridDyn), and [HELICS](https://github.com/GMLC-TDC/HELICS) and is currently a work in progress (though getting close). Namespaces, function names, and code organization is subject to change though is fairly stable at this point, input is welcome. A set of [documentation](https://units.readthedocs.io/en/latest/) is available. +This software was developed for use in [LLNL/GridDyn](https://github.com/LLNL/GridDyn), and [HELICS](https://github.com/GMLC-TDC/HELICS) and is currently a work in progress (though getting close). Namespaces, function names, and code organization is subject to change though is fairly stable at this point, input is welcome. A set of [documentation](https://units.readthedocs.io/en/latest/) is available. A Python wrapper is also available through [Pypi](https://pypi.org/project/units-llnl/) that wraps a limited subset of the library for most common purposes. ## Table of contents diff --git a/config/cppcheck_suppressions.txt b/config/cppcheck_suppressions.txt index 45e7b43a..8d7178a6 100644 --- a/config/cppcheck_suppressions.txt +++ b/config/cppcheck_suppressions.txt @@ -5,11 +5,11 @@ passedByValue:units/units.cpp:1160 passedByValue:units/units.cpp:1177 passedByValue:units/units.cpp:3067 passedByValue:units/units.hpp:413 -passedByValue:units/units.hpp:583 -passedByValue:units/units.hpp:1013 -passedByValue:units/units.hpp:1025 +passedByValue:units/units.hpp:590 +passedByValue:units/units.hpp:1020 +passedByValue:units/units.hpp:1032 returnByReference:units/units.hpp:363 -returnByReference:units/units.hpp:569 -returnByReference:units/units.hpp:1010 -returnByReference:units/units.hpp:1239 -returnByReference:units/units.hpp:1471 +returnByReference:units/units.hpp:576 +returnByReference:units/units.hpp:1017 +returnByReference:units/units.hpp:1246 +returnByReference:units/units.hpp:1485 diff --git a/docs/user-guide/index.rst b/docs/user-guide/index.rst index 7942c5a3..3359a1fb 100644 --- a/docs/user-guide/index.rst +++ b/docs/user-guide/index.rst @@ -5,7 +5,7 @@ User Guide The Units library user guide is an in depth look at how to use the C++ library and its functionality, covering the basic types in the library and operations with them. The guide covers the basic types and what operations are available on them, as well as a lot of details on how to use the library. -The python package is a simplified wrapper around the C++ library and is discussed in +The python package is a simplified wrapper around the C++ library and is discussed in :ref:`python` .. toctree:: :maxdepth: 1 diff --git a/docs/user-guide/python.rst b/docs/user-guide/python.rst index 07f92769..d2fa9f7f 100644 --- a/docs/user-guide/python.rst +++ b/docs/user-guide/python.rst @@ -2,7 +2,7 @@ Python ================== The Python wrapper for the units library is a simplified version of the library. It is focused on the string operations of the library and conversions between units and measurements. - +The two key classes are `Unit` which encapsulates a specific unit of measure and optional commodity if desired, and `Measurement` which captures a value + `Unit`. Math operations are supported on the types. The units library is available through a pypi_ package .. code-block:: sh diff --git a/pyproject.toml b/pyproject.toml index 8617500a..7ef2c5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "units_llnl" -version = "0.11.0" +version = "0.12.0" description = "Python bindings for the LLNL units library" readme = "python/README.md" requires-python = ">=3.10" @@ -14,7 +14,7 @@ license-files = [ "NOTICE" ] authors = [ - { name = "phlptp22", email = "top1@llnl.gov" }, + { name = "Philip Top", email = "top1@llnl.gov" }, ] classifiers = [ "Programming Language :: Python :: 3", @@ -38,6 +38,10 @@ Repository = "https://github.com/llnl/units" Issues = "https://github.com/LLNL/units/issues" Changelog = "https://github.com/LLNL/units/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +test = ["pytest"] + [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core minimum-version = "build-system.requires" diff --git a/python/README.md b/python/README.md index 207412cc..c1869ab2 100644 --- a/python/README.md +++ b/python/README.md @@ -4,6 +4,8 @@ [![Build Status](https://dev.azure.com/phlptp/units/_apis/build/status/LLNL.units?branchName=main)](https://dev.azure.com/phlptp/units/_build/latest?definitionId=1&branchName=main) [![CircleCI](https://circleci.com/gh/LLNL/units.svg?style=svg)](https://circleci.com/gh/LLNL/units) [![](https://img.shields.io/badge/License-BSD-blue.svg)](https://github.com/GMLC-TDC/HELICS-src/blob/main/LICENSE) +[![](https://img.shields.io/pypi/pyversions/units-llnl)](https://pypi.org/project/units-llnl/) +[![](https://img.shields.io/pypi/v/units-llnl)](https://pypi.org/project/units-llnl/) [![Documentation Status](https://readthedocs.org/projects/units/badge/?version=latest)](https://units.readthedocs.io/en/latest/?badge=latest) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/LLNL/units/main.svg)](https://results.pre-commit.ci/latest/github/LLNL/units/main) @@ -23,10 +25,12 @@ The Units library provides a means of working with units of measurement at runti - [Unit methods](#unit-methods) - [Constructors](#constructors) - [Methods](#methods) + - [Properties](#properties) - [Operators](#operators) - [Measurements](#measurements) - [Constructors](#constructors-1) - [Methods](#methods-1) + - [Properties](#properties-1) - [Operators](#operators-1) - [Other library methods](#other-library-methods) - [Future plans](#future-plans) @@ -87,38 +91,43 @@ These operations apply the `Units` object in Python. It maps to a `precise_unit` - `Unit(unit_str:str)` construct from a string - `Unit(unit_str:str,commodity_str:str)` construct a unit from a unit string and commodity string +- `Unit(float multiplier, unit:Unit)` construct a unit using another unit as a base along with a multiplier #### Methods -- `inv()->Unit` generate a new unit containing the inverse unit `Unit('m').inv()== Unit('1/m')` -- `pow(int power)->Unit` take a unit to power(NOTE: beware of limits on power representations of some units, things will always wrap so it is defined but may not produce what you expect). `power` can be negative. -- `is_exactly_the_same(other:Unit)->bool` compare two units and check for exact equivalence in both the unit_data and the multiplier -- `has_same_base(other:Unit)->bool` check if the units have the same base units -- `equivalent_non_counting(other:Unit)->bool` check if the units are equivalent ignoring the counting bases +- `is_exactly_the_same(other:Unit)->bool` compare two units and check for exact equivalence in both the unit_data and the multiplier. +- `has_same_base(other:Unit)->bool` check if the units have the same base units. +- `equivalent_non_counting(other:Unit)->bool` check if the units are equivalent ignoring the counting bases. - `is_convertible_to(other:Unit)->bool` check if the units are convertible to each other, currently checks `equivalent_non_counting()`, but some additional conditions might be allowed in the future to better match convert. -- `convert(value:float,unit_out:Unit|str)->float` convert a value from the existing unit to another, can also be a string -- `is_per_unit()->bool` true if the unit has the per_unit flag active -- `is_equation()->bool` true if the unit has the equation flag active -- `is_valid()->bool` true if the unit is a valid unit -- `is_normal()->bool` true if the unit is a normal unit (not error, nan, or subnormal) -- `is_error()->bool` true if the unit is an error unit (e.g invalid conversion) -- `isfinite()->bool` true if the unit does not have an infinite multiplier -- `isinf()->bool` true if the unit does have an infinite multiplier -- `root(power:int)->Unit` return a new unit taken to the root power -- `sqrt()->Unit` returns a new unit which is the square root of the current unit -- `to_string()->str` returns the string representation of the unit. This string is guaranteed to produce the same unit as the current unit, but may not be the same string as was used to create it. -- `multiplier()->float` return the unit multiplier as a floating point number -- `set_multiplier(mult:float)->Unit` generate a new Unit with the set multiplier -- `commodity()->str` get the commodity of the unit +- `convert(value:float,unit_out:Unit|str)->float` convert a value from the existing unit to another, can also be a string. +- `is_per_unit()->bool` true if the unit has the per_unit flag active. +- `is_equation()->bool` true if the unit has the equation flag active. +- `is_valid()->bool` true if the unit is a valid unit. +- `is_normal()->bool` true if the unit is a normal unit (not error, nan, or subnormal). +- `is_error()->bool` true if the unit is an error unit (e.g invalid conversion). +- `isfinite()->bool` true if the unit does not have an infinite multiplier. +- `isinf()->bool` true if the unit does have an infinite multiplier. +- `root(power:int)->Unit` return a new unit taken to the root power. +- `sqrt()->Unit` returns a new unit which is the square root of the current unit. +- `set_multiplier(mult:float)->Unit` generate a new Unit with the set multiplier. - `set_commodity(int commodity)` generate a new unit with the assigned commodity. +#### Properties + +- `multiplier->float` return the unit multiplier as a floating point number +- `commodity->str` get the commodity of the unit +- `base_units`->Unit gets the base units (no multiplier) associated with a unit + #### Operators - `*`,`/` with other units produces a new unit +- `~` produces the inverse of the unit - `**` is an exponentiation operator and produces a new unit - `*`, `/` with a floating point generates a `Measurement` - `==` and `!=` produce the appropriate comparison operators -- f string formatting also works with units +- f string formatting also works with units and returns the string representation of the unit. This string is guaranteed to produce the same unit as the current unit, but may not be the same string as was used to create it. +- `str`,`bool` are defined, `bool` indicates that the unit is valid, and non-zero +- `Units` may also be used as the indexing element in a dictionary ### Measurements @@ -129,30 +138,34 @@ These operations apply the `Units` object in Python. It maps to a `precise_unit` #### Methods -- `inv()->Unit` generate a new unit containing the inverse unit `Unit('m').inv()== Unit('1/m')` -- `pow(int power)->Unit` take a unit to power(NOTE: beware of limits on power representations of some units, things will always wrap so it is defined but may not produce what you expect). `power` can be negative. - `is_normal()->bool` true if the unit is a normal unit (not error, nan, or subnormal) - `is_valid()->bool` true if the `Measurement` is a valid Measurement (not error) - `root(power:int)->Measurement` return a new unit taken to the root power - `sqrt()->Unit` returns a new unit which is the square root of the current unit -- `to_string()->str` returns the string representation of the `Measurement`. This string is guaranteed to produce the equivalent `Measurement` as the current `Measurement`, but may not be the same string as was used to create it. -- `value()->float` return the numerical portion of a `Measurement` - `set_value(value:float)->Measurement` generate a new `Measurement` with the new Value -- `units()->Unit` get the `Unit` associated with a `Measurement` - `set_units(unit:Unit|str)` generate a new `Measurement` with the new units - `value_as(unit:Unit|str)->float` convert the value of the `Measurement` to a new `Unit` - `convert_to(unit:Unit|str)->Measurement` create a new `Measurement` with the new units and the value converted to those units - `convert_to_base()->Measurement` create a new `Measurement` with the units as the base measurement units - `is_close(other:Measurement)->bool` return true if the two measurements are close (both converted to non precise measurement and compared) +#### Properties + +- `value->float` return the numerical portion of a `Measurement` +- `units->Unit` get the `Unit` associated with a `Measurement` + #### Operators - `*`,`/` with other `Measurements` produces a new Measurement +- `~` inverts the measurement equivalent to `1/measurement` - `+`,`-` with other `Measurements` ensures the units are in the same base unit and performs the appropriate action -- `**` is an exponentiation operator and produces a new `Measurement` -- `*`, `/` with a floating point generates a `Measurement` +- `**` is an exponentiation operator and produces a new `Measurement` (NOTE: beware of limits on power representations of some units, things will always wrap so it is defined but may not produce what you expect). Can be negative. +- `*`, `/`,`%` with a floating point generates a `Measurement` +- `//` produces the floor of the resulting unit of division - `==`,`!=`,`>`,`<`,`>=`,`<=` produce the appropriate comparison operators -- f string formatting also works with units +- `str`,`float`,`bool` are defined, `bool` indicates that the measurement is non zero and is valid +- `round`, `math.ceil`,`math.floor`, and `math.trunc` work as expected +- f string formatting also works with measurement. Some special formatters are available `f"{m1:-}"` will remove the unit and just display the value. `f"{m1:new_unit}"` will convert to a new unit before displaying. `f"{m1:-new_unit}"` will do the conversion but just display the numerical value after the conversion. ### Other library methods @@ -164,7 +177,7 @@ These operations apply the `Units` object in Python. It maps to a `precise_unit` ### Future plans -Uncertain measurements will likely be added, along with some math operations on measurements (floor, ceil, round, etc). Also some more commodity operations, and x12 and r20 unit types. +Uncertain measurements will likely be added, potentially some trig functions on measurements. Also some more commodity operations, and x12 and r20 unit types. ## Contributions diff --git a/python/units_python.cpp b/python/units_python.cpp index e15cfa89..1359cf84 100644 --- a/python/units_python.cpp +++ b/python/units_python.cpp @@ -9,6 +9,7 @@ SPDX-License-Identifier: BSD-3-Clause #include #include "units/units.hpp" +#include "units/units_math.hpp" namespace nb = nanobind; using namespace nb::literals; @@ -32,6 +33,13 @@ NB_MODULE(units_llnl_ext, mod) new (type) units::precise_unit( units::unit_from_string(std::string(arg0))); }) + .def( + "__init__", + [](units::precise_unit* type, + double multiplier, + const units::precise_unit& base) { + new (type) units::precise_unit(multiplier, base); + }) .def( "__init__", [](units::precise_unit* type, @@ -41,15 +49,20 @@ NB_MODULE(units_llnl_ext, mod) units::unit_from_string(std::string(arg0)), units::getCommodity(std::string(commodity))}; }) - .def("multiplier", &units::precise_unit::multiplier) - .def( + .def_prop_ro("multiplier", &units::precise_unit::multiplier) + .def_prop_ro( "commodity", + [](const units::precise_unit& unit) { + return units::getCommodityName(unit.commodity()); + }) + .def_prop_ro( + "base_units", [](const units::precise_unit& type1) { - return units::getCommodityName(type1.commodity()); + return units::precise_unit(type1.base_units()); }) .def( "set_commodity", - [](units::precise_unit* unit, const char* commodity) { + [](const units::precise_unit* unit, const char* commodity) { return units::precise_unit( unit->multiplier(), unit->base_units(), @@ -57,11 +70,10 @@ NB_MODULE(units_llnl_ext, mod) }) .def( "set_multiplier", - [](units::precise_unit* unit, double mult) { + [](const units::precise_unit* unit, double mult) { return units::precise_unit( mult, unit->base_units(), unit->commodity()); }) - .def("inv", &units::precise_unit::inv) .def(nb::self * nb::self) .def(nb::self / nb::self) .def(float() * nb::self) @@ -72,7 +84,9 @@ NB_MODULE(units_llnl_ext, mod) .def(nb::self != nb::self) .def( "__pow__", - [](const units::precise_unit& a, int pow) { return a.pow(pow); }, + [](const units::precise_unit& unit, int pow) { + return unit.pow(pow); + }, nb::is_operator()) .def( "is_exactly_the_same", @@ -135,45 +149,55 @@ NB_MODULE(units_llnl_ext, mod) .def("is_equation", &units::precise_unit::is_equation) .def( "is_valid", - [](const units::precise_unit& type) { - return units::is_valid(type); + [](const units::precise_unit& unit) { + return units::is_valid(unit); }) .def( "is_normal", - [](const units::precise_unit& type) { - return units::isnormal(type); + [](const units::precise_unit& unit) { + return units::isnormal(unit); }) .def( "is_error", - [](const units::precise_unit& type) { - return units::is_error(type); + [](const units::precise_unit& unit) { + return units::is_error(unit); }, "return true if the unit has the error flags set or is infinite") .def( "isfinite", - [](const units::precise_unit& type) { - return units::isfinite(type); + [](const units::precise_unit& unit) { + return units::isfinite(unit); }) .def( "isinf", - [](const units::precise_unit& type) { return units::isinf(type); }) + [](const units::precise_unit& unit) { return units::isinf(unit); }) .def( "root", - [](const units::precise_unit& type, int root) { - return units::root(type, root); + [](const units::precise_unit& unit, int root) { + return units::root(unit, root); }) .def( "sqrt", - [](const units::precise_unit& type) { - return units::root(type, 2); + [](const units::precise_unit& unit) { + return units::root(unit, 2); }) + .def( + "__invert__", + [](const units::precise_unit& unit) { return unit.inv(); }) .def( "__repr__", - [](const units::precise_unit& type) { - return units::to_string(type); + [](const units::precise_unit& unit) { + return units::to_string(unit); + }) + .def( + "__bool__", + [](const units::precise_unit& unit) { + return ( + is_valid(unit) && !is_error(unit) && + unit.multiplier() != 0); }) - .def("to_string", [](const units::precise_unit& type) { - return units::to_string(type); + .def("__hash__", [](const units::precise_unit& unit) { + return std::hash()(unit); }); nb::class_( @@ -202,16 +226,24 @@ NB_MODULE(units_llnl_ext, mod) const units::precise_unit& unit) { new (measurement) units::precise_measurement(value, unit); }) - .def("value", &units::precise_measurement::value) + .def_prop_ro( + "value", + [](const units::precise_measurement& measurement) { + return measurement.value(); + }) .def( "set_value", - [](units::precise_measurement* measurement, double value) { + [](const units::precise_measurement* measurement, double value) { return units::precise_measurement(value, measurement->units()); }) - .def("units", &units::precise_measurement::units) + .def_prop_ro( + "units", + [](const units::precise_measurement& measurement) { + return measurement.units(); + }) .def( "set_units", - [](units::precise_measurement* measurement, + [](const units::precise_measurement* measurement, const units::precise_unit& unit) { return units::precise_measurement(measurement->value(), unit); }) @@ -257,12 +289,14 @@ NB_MODULE(units_llnl_ext, mod) .def("as_unit", &units::precise_measurement::as_unit) .def(nb::self * nb::self) .def(nb::self / nb::self) + .def(nb::self % nb::self) .def(nb::self + nb::self) .def(nb::self - nb::self) .def(float() * nb::self) .def(nb::self * float()) .def(float() / nb::self) .def(nb::self / float()) + .def(nb::self % float()) .def(nb::self == nb::self) .def(nb::self != nb::self) .def(nb::self > nb::self) @@ -275,6 +309,23 @@ NB_MODULE(units_llnl_ext, mod) return pow(measurement, power); }, nb::is_operator()) + .def( + "__floordiv__", + [](const units::precise_measurement& measurement, + const units::precise_measurement& other) { + auto res1 = measurement / other; + if (res1.units().unit_type_count() == 0) { + res1 = res1.convert_to_base(); + } + return floor(res1); + }, + nb::is_operator()) + .def( + "__floordiv__", + [](const units::precise_measurement& measurement, double divisor) { + return floor(measurement / divisor); + }, + nb::is_operator()) .def( "is_valid", [](const units::precise_measurement& measurement) { @@ -310,8 +361,67 @@ NB_MODULE(units_llnl_ext, mod) [](const units::precise_measurement& measurement) { return units::to_string(measurement); }) - .def("to_string", [](const units::precise_measurement& measurement) { - return units::to_string(measurement); + .def( + "__format__", + [](const units::precise_measurement& measurement, + std::string fmt_string) { + if (fmt_string.empty()) { + return units::to_string(measurement); + } + if (fmt_string == "-") { + return units::to_string(units::precise_measurement( + measurement.value(), units::precise::one)); + } + if (fmt_string.front() == '-') { + return units::to_string(units::precise_measurement( + measurement.value_as( + units::unit_from_string(fmt_string.substr(1))), + units::precise::one)); + } else { + return units::to_string(measurement.convert_to( + units::unit_from_string(fmt_string))); + } + }) + .def( + "__neg__", + [](const units::precise_measurement& measurement) { + return -measurement; + }) + .def( + "__invert__", + [](const units::precise_measurement& measurement) { + return 1.0 / measurement; + }) + .def( + "__trunc__", + [](const units::precise_measurement& measurement) { + return trunc(measurement); + }) + .def( + "__ceil__", + [](const units::precise_measurement& measurement) { + return ceil(measurement); + }) + .def( + "__floor__", + [](const units::precise_measurement& measurement) { + return floor(measurement); + }) + .def( + "__round__", + [](const units::precise_measurement& measurement) { + return round(measurement); + }) + .def( + "__float__", + [](const units::precise_measurement& measurement) { + return measurement.value(); + }) + .def("__bool__", [](const units::precise_measurement& measurement) { + return ( + is_valid(measurement.units()) && (measurement.value() != 0.0) && + (measurement.units().multiplier() != 0.0) && + !is_error(measurement.units())); }); mod.def( diff --git a/test/python/test_measurement.py b/test/python/test_measurement.py index 611d1fa2..9e250515 100644 --- a/test/python/test_measurement.py +++ b/test/python/test_measurement.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause import units_llnl as u +import math def test_basic_measurement(): @@ -32,7 +33,7 @@ def test_basic_measurement3(): m4 = u.Measurement(4.0, u1 / u2) assert m1 / m2 == m4 - assert m4.units() == u1 / u2 + assert m4.units == u1 / u2 def test_conditions(): @@ -80,16 +81,16 @@ def test_comparisons(): def test_set_value(): m1 = u.Measurement("100 months") - assert m1.value() == 100 + assert m1.value == 100 m2 = m1.set_value(14) - assert m2.value() == 14 + assert m2.value == 14 def test_set_units(): m1 = u.Measurement("100 months") - assert m1.units() == u.Unit("month") + assert m1.units == u.Unit("month") m2 = m1.set_units(u.Unit("day")) - assert m2.units() == u.Unit("day") + assert m2.units == u.Unit("day") def test_value_as(): @@ -102,40 +103,91 @@ def test_value_as(): def test_convert_to(): m1 = u.Measurement("20 weeks") m2 = m1.convert_to("day") - assert m2.value() == 20 * 7 + assert m2.value == 20 * 7 u1 = u.Unit("hr") m3 = m1.convert_to("hr") - assert m3.value() == 20 * 7 * 24 + assert m3.value == 20 * 7 * 24 m4 = m1.convert_to_base() - assert m4.units() == u.Unit("s") - assert m4.units().multiplier() == 1.0 - assert m4.value() == 20 * 7 * 24 * 3600 + assert m4.units == u.Unit("s") + assert m4.units.multiplier == 1.0 + assert m4.value == 20 * 7 * 24 * 3600 def test_as_unit(): m1 = u.Measurement("15 seconds") m2 = u.Measurement(4, m1.as_unit()) - assert m2.value() == 4 + assert m2.value == 4 assert m2.value_as("s") == 60 + assert float(m1) == 15 + assert m1 def test_add_sub(): m1 = u.Measurement("15 seconds") m2 = u.Measurement(1, "minute") m3 = m2 - m1 - assert m3.value() == 0.75 + assert m3.value == 0.75 m4 = m3 + m2 + m1 assert m4 == u.Measurement(120, "second") +def test_negation(): + m1 = u.Measurement("15 seconds") + m3 = -m1 + assert m3.value == -15.0 + + +def test_conditions(): + m1 = u.Measurement(34.5, "fq2te1tg1fe") + assert not m1 + assert not bool(m1) + + m2 = u.Measurement(0, "m") + assert not m2 + assert not bool(m2) + assert m2.is_normal() + + +def test_mod(): + m1 = u.Measurement("18 seconds") + m2 = u.Measurement("1 min") + m3 = (m2 % m1).convert_to("s") + assert math.floor(m3.value) == 6 + m4 = m1 % 5 + assert m4.value == 3 + + +def test_floor_div(): + m1 = u.Measurement("18 seconds") + m2 = u.Measurement("1 min") + m3 = m2 // m1 + assert m3.value == 3 + m4 = m1 // 4 + assert m4.value == 4 + assert m4.units == u.Unit("s") + + +def test_math_func(): + m1 = u.Measurement("15.78 seconds") + m2 = u.Measurement("15.48 seconds") + assert math.floor(m1).value == 15 + assert math.floor(m2).value == 15 + assert math.ceil(m1).value == 16 + assert math.ceil(m2).value == 16 + assert round(m1).value == 16 + assert round(m2).value == 15 + assert math.trunc(m1).value == 15 + assert math.trunc(m2).value == 15 + + def test_mult(): m1 = u.Measurement("2 meters") m2 = u.Measurement(3, "meters") m3 = m2 * m1 - assert m3.value() == 6 + assert m3.value == 6 m4 = 3 * m3 assert m4 == u.Measurement(18, "meters squared") @@ -148,7 +200,7 @@ def test_div(): m1 = u.Measurement("10 meters") m2 = u.Measurement(2, "seconds") m3 = m1 / m2 - assert m3.value() == 5 + assert m3.value == 5 m4 = 10 / m3 assert m4 == u.Measurement(2, "s/m") @@ -159,11 +211,23 @@ def test_div(): def test_string(): m1 = u.Measurement("10 lb") - assert m1.to_string() == "10 lb" + assert str(m1) == "10 lb" s3 = f"the measurement is {m1}" assert s3 == "the measurement is 10 lb" +def test_format(): + m1 = u.Measurement("9.7552 lb") + s1 = f"the measurement is {m1:kg}" + assert "kg" in s1 + + s2 = f"the measurement is {m1:-}" + assert s2 == "the measurement is 9.7552 " + + s3 = f"the measurement is {m1:-kg}" + assert "kg" not in s3 + + def test_close(): m1 = u.Measurement("10 lb") m2 = u.Measurement("10.0000000001 lb") diff --git a/test/python/test_units.py b/test/python/test_units.py index 7e79aa7c..723f9305 100644 --- a/test/python/test_units.py +++ b/test/python/test_units.py @@ -14,6 +14,14 @@ def test_basic_unit(): assert u3.is_convertible_to(u4) +def test_unit_constructor(): + u1 = u.Unit("cm") + u2 = u.Unit(100, u1) + + assert u2 == u.Unit("m") + assert u2 + + def test_basic_multiplication(): u1 = u.Unit("m") u2 = u.Unit("s") @@ -26,10 +34,11 @@ def test_conditions(): u2 = u.Unit("error") u3 = u.Unit("infinity") assert not u1.is_error() + assert bool(u1) assert u2.is_error() assert u1.is_normal() assert not u2.is_normal() - + assert not bool(u2) assert u1.is_valid() assert u1.isfinite() @@ -43,6 +52,27 @@ def test_conditions(): assert not u3.is_per_unit() +def test_bad_unit(): + ue = u.Unit("qdfgqtegqgqg") + assert not ue + assert ue.is_error() + + +def test_zero(): + u1 = u.Unit("0") + u2 = u.Unit("nan") + u3 = u.Unit("m/s") + u4 = u.Unit(0, u3) + assert not bool(u1) + assert not u1 + assert not bool(u2) + assert not u2 + assert u3 + assert bool(u3) + assert not bool(u4) + assert not u4 + + def test_root(): u1 = u.Unit("m^6 per second squared") u2 = u1.root(2) @@ -64,45 +94,69 @@ def test_base(): def test_multiplier(): u1 = u.Unit("nm") - assert u1.multiplier() == 1e-9 + assert u1.multiplier == 1e-9 u2 = u1.set_multiplier(1e-6) assert u2 == u.Unit("um") - assert u2.multiplier() == 1e-6 + assert u2.multiplier == 1e-6 assert u1 != u2 def test_commodity(): u1 = u.Unit("lb", "gold") - assert u1.commodity() == "gold" + assert u1.commodity == "gold" u2 = u1.set_commodity("silver") - assert u2.commodity() == "silver" + assert u2.commodity == "silver" def test_string(): u1 = u.Unit("lb") - assert u1.to_string() == "lb" + assert str(u1) == "lb" s3 = f"the unit is {u1}" assert s3 == "the unit is lb" u3 = u.Unit() - assert u3.to_string() == "" + assert str(u3) == "" def test_inv(): u1 = u.Unit("s") - assert u1.inv() == u.Unit("Hz") - u3 = u1.inv().inv() + assert ~u1 == u.Unit("Hz") + u3 = ~(~u1) assert u3 == u1 +def test_hash(): + u1 = u.Unit("25.6 in") + u2 = u1 + u3 = u.Unit("11.3 m") + h1 = hash(u1) + h2 = hash(u2) + h3 = hash(u3) + assert h1 == h2 + assert h1 != h3 + + +def test_dictionary(): + d1 = {} + + u1 = u.Unit("25.6 in") + u2 = u1 + u3 = u.Unit("11.3 m") + d1[u1] = "in" + d1[u3] = "m" + + assert d1[u2] == "in" + assert d1[u3] == "m" + + def test_float_mult(): u1 = u.Unit("m") m3 = 10 * u1 assert type(m3).__name__ == "Measurement" - assert m3.value() == 10 + assert m3.value == 10 m4 = u1 * 12 assert type(m4).__name__ == "Measurement" - assert m4.value() == 12 + assert m4.value == 12 def test_convert_units(): diff --git a/test/python/test_user_defined.py b/test/python/test_user_defined.py index 4979c149..91d5e878 100644 --- a/test/python/test_user_defined.py +++ b/test/python/test_user_defined.py @@ -15,12 +15,12 @@ def test_user_defined_unit(): assert Unit("clucks/A") == Unit("19.3 m") assert Unit("sclucks/$") == Unit("23 m*mol") - assert clucks.to_string() == "clucks" - assert clucks.inv().to_string() == "1/clucks" + assert str(clucks) == "clucks" + assert str(~clucks) == "1/clucks" sclucks = Unit("sclucks") assert not sclucks.is_error() - assert (Unit("ug") / sclucks**3).to_string() == "ug/sclucks^3" + assert str(Unit("ug") / sclucks**3) == "ug/sclucks^3" def test_user_defined_unit_from_file(): diff --git a/units/units.hpp b/units/units.hpp index fac22e17..f0affe80 100644 --- a/units/units.hpp +++ b/units/units.hpp @@ -532,7 +532,14 @@ class fixed_measurement { { return {value_ / val, units_}; } - + fixed_measurement operator%(const fixed_measurement& other) const + { + return {fmod(value_, other.value_as(units_)), units_}; + } + fixed_measurement operator%(double val) const + { + return {fmod(value_, val), units_}; + } fixed_measurement operator+(const measurement& other) const { return {value_ + other.value_as(units_), units_}; @@ -1267,7 +1274,14 @@ class precise_measurement { { return {value_ / val, units_}; } - + precise_measurement operator%(const precise_measurement& other) const + { + return {fmod(value_, other.value_as(units_)), units_}; + } + precise_measurement operator%(double val) const + { + return {fmod(value_, val), units_}; + } precise_measurement operator+(const precise_measurement& other) const { return {value_ + other.value_as(units_), units_}; @@ -1507,6 +1521,14 @@ class fixed_precise_measurement { { return {value_ / val, units_}; } + fixed_precise_measurement operator%(const precise_measurement& other) const + { + return {fmod(value_, other.value_as(units_)), units_}; + } + fixed_precise_measurement operator%(double val) const + { + return {fmod(value_, val), units_}; + } fixed_precise_measurement operator+(const precise_measurement& other) const {