diff --git a/xo-unit/.github/workflows/nix-main.yml b/xo-unit/.github/workflows/nix-main.yml new file mode 100644 index 00000000..f0ae63e3 --- /dev/null +++ b/xo-unit/.github/workflows/nix-main.yml @@ -0,0 +1,51 @@ +# Workflow to build xo-unit using custom docker container; +# container provides nix support +# +# NOTES +# 1. GIT_TOKEN granted automatically by github. +# has read permission on public resources + read/write permission on this repo +# +# 2. container built from [[https:github.com:rconybea/docker-nix-builder]] +# Includes dependencies: +# - nix +# - compiler toolchain: gcc, binutils, bash, etc +# - git +# - cmake +# - catch2 +# - pybind11 + python +# - libwebsockets +# - jsoncpp +# +name: xo-unit nix builder + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + BUILD_TYPE: Release + +jobs: + build_job: + name: xo-unit nix build on docker-nix-builder + runs-on: ubuntu-latest + container: + # custom docker image. see github.com:rconybea/docker-nix-builder for definition + image: ghcr.io/rconybea/docker-nix-builder:v1 + + steps: + # not using usual checkout actions: they don't work out-of-the-box from within a container + + - name: xo-unit + run: | + echo "::group::clone xo-unit repo" + mkdir -p repo + GIT_SSL_NO_VERIFY=true git clone https://${{env.GIT_USER}}:${{env.GIT_TOKEN}}@github.com/rconybea/xo-unit.git repo/xo-unit + echo "::endgroup" + + echo "::group::build xo-unit with nix" + export NIXPKGS_ALLOW_UNFREE=1 + (cd repo/xo-unit && nix build --impure -L --print-build-logs .#xo-unit && tree ./result) + echo "::endgroup" diff --git a/xo-unit/.github/workflows/ubuntu-main.yml b/xo-unit/.github/workflows/ubuntu-main.yml new file mode 100644 index 00000000..fadde45d --- /dev/null +++ b/xo-unit/.github/workflows/ubuntu-main.yml @@ -0,0 +1,252 @@ +name: build xo-unit + dependencies + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + steps: + - name: checkout source + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + # install catch2, doxygen. see + # [[https://stackoverflow.com/questions/57982945/how-to-apt-get-install-in-a-github-actions-workflow]] + + echo "::group::install catch2" + sudo apt-get install -y catch2 + echo "::endgroup" + + echo "::group::install doxygen" + sudo apt-get install -y doxygen + echo "::endgroup" + + echo "::group::install sphinx" + sudo apt-get install -y python3-sphinx + echo "::endgroup" + + echo "::group::install sphinx readthedocs theme" + sudo apt-get install -y python3-sphinx-rtd-theme + echo "::endgroup" + + #echo "::group::install pybind11" + #sudo apt-get install -y pybind11-dev + #echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: clone xo-cmake + uses: actions/checkout@v3 + with: + repository: Rconybea/xo-cmake + path: repo/xo-cmake + + - name: build xo-cmake + run: | + XONAME=xo-cmake + XOSRC=repo/${XONAME} + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${XOSRC} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: clone xo-indentlog + uses: actions/checkout@v3 + with: + repository: Rconybea/indentlog + path: repo/xo-indentlog + + - name: build xo-indentlog + run: | + XONAME=xo-indentlog + XOSRC=repo/${XONAME} + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::repo dir tree" + tree -L 2 repo + echo "::endgroup" + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${XOSRC} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} -j + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree -L 3 ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: clone xo-flatstring + uses: actions/checkout@v3 + with: + repository: Rconybea/xo-flatstring + path: repo/xo-flatstring + + - name: build xo-flatstring + run: | + XONAME=xo-flatstring + XOSRC=repo/${XONAME} + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::repo dir tree" + tree -L 2 repo + echo "::endgroup" + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${XOSRC} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} -j + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree -L 3 ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: clone xo-randomgen + uses: actions/checkout@v3 + with: + repository: Rconybea/randomgen + path: repo/xo-randomgen + + - name: build xo-randomgen + run: | + XONAME=xo-randomgen + XOSRC=repo/${XONAME} + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::repo dir tree" + tree -L 2 repo + echo "::endgroup" + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${XOSRC} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} -j + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree -L 3 ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: clone xo-ratio + uses: actions/checkout@v3 + with: + repository: Rconybea/xo-ratio + path: repo/xo-ratio + + - name: build xo-ratio + run: | + XONAME=xo-ratio + XOSRC=repo/${XONAME} + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::repo dir tree" + tree -L 2 repo + echo "::endgroup" + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${XOSRC} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} -j + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree -L 3 ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: build self (xo-unit) + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: | + XONAME=xo-unit + BUILDDIR=${{github.workspace}}/build_${XONAME} + PREFIX=${{github.workspace}}/local + + echo "::group::repo dir tree" + tree -L 2 repo + echo "::endgroup" + + echo "::group::configure ${XONAME}" + cmake -B ${BUILDDIR} -DCMAKE_MODULE_PATH=${PREFIX}/share/cmake -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + echo "::endgroup" + + echo "::group::compile ${XONAME}" + cmake --build ${BUILDDIR} --config ${{env.BUILD_TYPE}} + echo "::endgroup" + + echo "::group::local install ${XONAME}" + cmake --install ${BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree -L 3 ${PREFIX} + echo "::endgroup" + + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + (cd ${BUILDDIR} && ctest -C ${{env.BUILD_TYPE}}) diff --git a/xo-unit/.github/workflows/xo-cpp-main.yml b/xo-unit/.github/workflows/xo-cpp-main.yml new file mode 100644 index 00000000..ecb0f614 --- /dev/null +++ b/xo-unit/.github/workflows/xo-cpp-main.yml @@ -0,0 +1,248 @@ +name: XO unit builder + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + container: + # custom docker image. see github.com:rconybea/docker-xo-builder for definition + image: ghcr.io/rconybea/docker-xo-builder:v1 + + steps: + - name: xo-cmake + run: | + # treat github.com as known host to prevent shtoopid SSL errors + mkdir -p ~/.ssh + ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts + cat ~/.ssh/known_hosts + + git config --global http.sslVerify false + + XO_NAME=xo-cmake + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/xo-cmake.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: xo-indentlog + run: | + XO_NAME=xo-indentlog + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/indentlog.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: xo-flatstring + run: | + XO_NAME=xo-flatstring + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/${XO_NAME}.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: xo-randomgen + run: | + XO_NAME=xo-randomgen + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/randomgen.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: xo-ratio + run: | + XO_NAME=xo-ratio + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/${XO_NAME}.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" + + # ---------------------------------------------------------------- + + - name: xo-unit + run: | + XO_NAME=xo-unit + XO_SRC=repo/${XO_NAME} + XO_BUILDDIR=${{github.workspace}}/build_${XO_NAME} + PREFIX=${{github.workspace}}/local + + XO_REPO=https://github.com/rconybea/${XO_NAME}.git + + mkdir -p ${XO_SRC} + mkdir -p ${XO_BUILDDIR} + + echo "::group::clone ${XO_NAME}" + export GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' + export GIT_SSL_NOVERIFY=true + git clone ${XO_REPO} ${XO_SRC} + echo "::endgroup" + + echo "::group::configure ${XO_NAME}" + cmake -B ${XO_BUILDDIR} -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${XO_SRC} + echo "::endgroup" + + echo "::group::compile ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -j + echo "::endgroup" + + echo "::group::test ${XO_NAME}" + cmake --build ${XO_BUILDDIR} -- test + echo "::endgroup" + + echo "::group::local install ${XO_NAME}" + cmake --install ${XO_BUILDDIR} + echo "::endgroup" + + echo "::group::local dir tree" + tree ${PREFIX} + echo "::endgroup" diff --git a/xo-unit/.gitignore b/xo-unit/.gitignore new file mode 100644 index 00000000..f3b23fc3 --- /dev/null +++ b/xo-unit/.gitignore @@ -0,0 +1,8 @@ +# emacs projectile config +.projectile +# clangd working space (see emacs+lsp) +.cache +# typical cmake build directory (source-tree-nephew) +.build* +# symlink to builddir/compile_commands.json; should be set manually in dev sandbox +compile_commands.json diff --git a/xo-unit/CMakeLists.txt b/xo-unit/CMakeLists.txt new file mode 100644 index 00000000..50e70f0b --- /dev/null +++ b/xo-unit/CMakeLists.txt @@ -0,0 +1,43 @@ +# xo-unit/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_unit VERSION 1.0) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +# one-time project-specific c++ flags. usually empty +set(PROJECT_CXX_FLAGS "-Wstringop-overread") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- + +add_subdirectory(example) +add_subdirectory(utest) + +set(SELF_LIB xo_unit) +xo_add_headeronly_library(${SELF_LIB}) +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# docs targets depend on all the other library/utest targets +# +add_subdirectory(docs) + +# ---------------------------------------------------------------- +# dependencies + +# for some reason, this doesn't reliably bring in xo_flatstring dependency +xo_headeronly_dependency(${SELF_LIB} xo_ratio) +xo_headeronly_dependency(${SELF_LIB} xo_flatstring) +# etc.. + +# end CMakeLists.txt diff --git a/xo-unit/LICENSE b/xo-unit/LICENSE new file mode 100644 index 00000000..cae3cb5d --- /dev/null +++ b/xo-unit/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2024 Roland Conybeare , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. diff --git a/xo-unit/README.md b/xo-unit/README.md new file mode 100644 index 00000000..66792cea --- /dev/null +++ b/xo-unit/README.md @@ -0,0 +1,114 @@ +# unit library + +Provides compile-time dimension checking and scaling. + +Similar in spirit to `boost::units`, but: +1. streamlined: assumes modern (c++20) support +2. supports fractional dimensions (rational powers) + +## Documentation + +- xo-unit documentation [under construction]: [documentation](https://rconybea.github.io/web/xo-unit/html/index.html) +- unit test coverage here: [coverage](https://rconybea.github.io/web/xo-unit/ccov/html/index.html) + +## Example + +``` +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" + +namespace q = xo::qty::qty; +namespace u = xo::qty::u; + +constexpr auto t = q::minutes(2); +constexpr auto d = q::kilometers(2.5); + +constexpr auto t2 = t*t; // unit will be min^-2 +constexpr auto a = d / t2; // unit will be km.min^-2 + +// convert to m.s^-2 +constexpr quantity a2 = a; + +//constexpr quantity a3 = a; // dimension mismatch, will not compile + +// get dimensionless scale value +double x = a2.scale(); +``` + +## Getting Started + +See [full install instructions](https://rconybea.github.io/web/xo-unit/html/install.html) for other installation strategies. + +### build + install `xo-cmake` dependency + +- [github/Rconybea/xo-cmake](https://github.com/Rconybea/xo-cmake) + +Installs a few cmake ingredients, along with build assistant `xo-build` for XO projects such as this one. + +### build + install other XO dependencies +``` +$ xo-build --clone --configure --build --install xo-flatstring +$ xo-build --clone --configure --build --install xo-ratio +``` + +Note: can use `-n` to dry-run here + +### copy `xo-unit` repository locally +``` +$ xo-build --clone xo-unit +``` + +or equivalently +``` +$ git clone https://github.com/rconybea/xo-unit +``` + +### build + install `xo-unit` +``` +$ xo-build --configure --build --install xo-unit +``` + +or equivalently: +``` +$ PREFIX=/usr/local # or wherever you prefer +$ cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} -S xo-unit -B xo-unit/.build +$ cmake --build xo-unit/.build -j +$ cmake --install xo-unit/.build +``` + +### build documentation +``` +$ cd xo-unit +$ cmake -DCMAKE_INSTALL_PREFIX=${PREFIX} --build .build -- sphinx +``` +When this completes, can point local browser to `xo-unit/.build/docs/sphinx/index.html`. + +### build for unit test coverage + +(Note that unit tests involve additional dependencies): +``` +$ xo-build --clone --configure --build --install xo-indentlog +$ xo-build --clone --configure --build --install xo-randomgen +``` + +``` +$ cmake -DCMAKE_BUILD_TYPE=coverage -DCMAKE_INSTALL_PREFIX=$PREFIX -DENABLE_TESTING=1 xo-unit/.build-ccov +$ cmake --build xo-unit/.build-ccov +``` + +run coverage-enabled unit tests: +``` +$ cmake --build xo-unit/.build-ccov -- test +``` + +generate html+text coverage report: +``` +$ xo-unit/.build-ccov/gen-ccov +``` +To see coverage, can point local browser to `xo-unit/.build-ccov/ccov/html/index.html` + +### LSP support +``` +$ cd xo-unit +$ ln -s .build/compile_commands.json # lsp will look for compile_commands.json in the root of the source tree +``` diff --git a/xo-unit/cmake/gen-ccov.in b/xo-unit/cmake/gen-ccov.in new file mode 100644 index 00000000..e335aed4 --- /dev/null +++ b/xo-unit/cmake/gen-ccov.in @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +srcdir=@PROJECT_SOURCE_DIR@ +builddir=@PROJECT_BINARY_DIR@ +lcov=@LCOV_EXECUTABLE@ +genhtml=@GENHTML_EXECUTABLE@ + +if [[ $lcov == "LCOV_EXECUTABLE-NOTFOUND" ]]; then + echo "gen-ccov: lcov executable not found" + exit 1 +fi + +if [[ $genhtml == "GENHTML_EXECUTABLE-NOTFOUND" ]]; then + echo "gen-ccov: genhtml executable not found" + exit 1 +fi + +mkdir $builddir/ccov + +$srcdir/cmake/lcov-harness $srcdir $builddir $builddir/ccov/out $lcov $genhtml diff --git a/xo-unit/cmake/lcov-harness b/xo-unit/cmake/lcov-harness new file mode 100755 index 00000000..27ac8be9 --- /dev/null +++ b/xo-unit/cmake/lcov-harness @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +srcdir=$1 +builddir=$2 +outputstem=$3 +lcov=$4 +genhtml=$5 + +if [[ -z "${srcdir}" ]]; then + echo "lcov-harness: expected non-empty srcdir" + exit 1 +fi + +if [[ -z ${builddir} ]]; then + echo "lcov-harness: expected non-empty builddir" + exit 1 +fi + +if [[ -z ${outputstem} ]]; then + echo "lcov-harness: expected non-empty outputstem" + exit 1 +fi + +if [[ -z ${lcov} ]]; then + echo "lcov-harness: exepcted non-empty lcov" + exit 1 +fi + +if [[ -z ${genhtml} ]]; then + echo "lcov-harness: expected non-empty genhtml" + exit 1 +fi + +# directory stems for location of {.gcda, gcno} coverage information, +# +# if we have source tree: +# +# ${srcdir} +# +- foo +# | \- foo.cpp +# \- bar +# \- quux +# +- quux.cpp +# \- quux_main.cpp +# +# then we expect build tree: +# +# ${builddir} +# +- foo +# | \- CMakeFiles +# | \- foo_target.dir +# | +- foo.cpp.gcda +# | \- foo.cpp.gcno +# +- bar +# \- quux +# \- CMakeFiles +# \- target4quux.dir +# +- quux.cpp.gcda +# +- quux.cpp.gcno +# +- quux_main.cpp.gcda +# \- quux_main.cpp.gcno +# +# in which case will have cmd_body: +# +# ${primarydirs} +# ./foo/CMakeFiles/foo_target.dir +# ./bar/quux/CMakeFiles/target4quux.dir +# +# here foo_target, quux_target are whatever build is using for corresponding cmake target names. +# +# We want to invoke lcov like: +# +# lcov --capture \ +# --output ${builddir}/ccov \ +# --exclude /utest/ \ +# --base-directory ${srcdir}/foo --directory ${builddir}/foo/CMakeFiles/foo_target.dir \ +# --base-directory ${srcdir}/bar/quux --directory ${builddir}/bar/quux/CMakeFiles/target4quux.dir +# +primarydirs=$(cd ${builddir} && find -name '*.gcno' \ + | xargs --replace=xx dirname xx \ + | uniq \ + | sed -e 's:^\./::') + +#echo "primarydirs=${primarydirs}" + +cmd="${lcov} --output ${outputstem}.info --capture --ignore-errors source" + +for bdir in ${primarydirs}; do + sdir=$(dirname $(dirname ${bdir})) + + cmd="${cmd} --base-directory ${srcdir}/${sdir} --directory ${builddir}/${bdir}" +done + +#echo cmd=${cmd} + +set -x + +# capture +${cmd} + +# keep only files with paths under source tree +# (don't want coverage for external libraries such as libstdc++ etc) +${lcov} --extract ${outputstem}.info "${srcdir}/*" --output ${outputstem}2.info + +# remove unit test dirs +# (we're interested in coverage of our installed code, not of the unit tests that exercise it) +${lcov} --remove ${outputstem}2.info '*/utest/*' --output ${outputstem}3.info + +# generate .html tree +mkdir -p ${builddir}/ccov/html +${genhtml} --ignore-errors source --show-details --prefix ${srcdir} --output-directory ${builddir}/ccov/html ${outputstem}3.info + +# also send report to stdout +${lcov} --list ${outputstem}3.info diff --git a/xo-unit/cmake/xo-bootstrap-macros.cmake b/xo-unit/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..aba31169 --- /dev/null +++ b/xo-unit/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------- +# for example: +# $ PREFIX=/usr/local # for example +# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build +# +# will get +# CMAKE_MODULE_PATH +# from xo-cmake-config --cmake-module-path +# +# and expect .cmake macros in +# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake +# ---------------------------------------------------------------- + +find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED) + +if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND") + message(FATAL "could not find xo-cmake-config executable") +endif() + +message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + +if (NOT XO_SUBMODULE_BUILD) + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # default to typical install location for xo-project-macros + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH) + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo_cxx) + +xo_cxx_bootstrap_message() diff --git a/xo-unit/cmake/xo_unitConfig.cmake.in b/xo-unit/cmake/xo_unitConfig.cmake.in new file mode 100644 index 00000000..871a9b44 --- /dev/null +++ b/xo-unit/cmake/xo_unitConfig.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +find_dependency(xo_ratio) +find_dependency(indentlog) +#find_dependency(printjson) +#find_dependency(callback) + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/xo-unit/docs/CMakeLists.txt b/xo-unit/docs/CMakeLists.txt new file mode 100644 index 00000000..77960652 --- /dev/null +++ b/xo-unit/docs/CMakeLists.txt @@ -0,0 +1,17 @@ +# xo-unit/docs/CMakeLists.txt + +xo_doxygen_collect_deps() +xo_docdir_doxygen_config() +xo_docdir_sphinx_config( + index.rst examples.rst glossary.rst install.rst implementation.rst + quantity-reference.rst quantity-class.rst quantity-factoryfunctions.rst quantity-unitvars.rst quantity-source-code.rst + xquantity-reference.rst xquantity-class.rst xquantity-source-code.rst + scaled-unit-reference.rst scaled-unit-class.rst scaled-unit-constants.rst + natural-unit-class.rst + bpu-class.rst + bu-store-class.rst basis-unit-reference.rst + basis-unit-class.rst basis-unit-constants.rst + dimension-enum.rst + development.rst + ubuntu-github-workflow.rst +) diff --git a/xo-unit/docs/README b/xo-unit/docs/README new file mode 100644 index 00000000..2fab6399 --- /dev/null +++ b/xo-unit/docs/README @@ -0,0 +1,70 @@ +build + + +-----------------------------------------------+ + | cmake | + | CMakeLists.txt | + | $PREFIX/share/cmake/xo_macros/xo_cxx.cmake | + +-----------------------------------------------+ + | + | +----------------------+ + +------------------------------------------------->| .build/docs/Doxyfile | + | +----------------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +-----------------+ + +---->| doxygen |--->| .build/docs/dox | + | | $PREFIX/share/xo-macros/Doxyfile.in | | +- html/ | + | +---------------------------------------+ | +- xml/ | + | +-----------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +--------------------+ + \---->| sphinx |--->| .build/docs/sphinx | + | +- conf.py | | +- html/ | + | +- _static/ | +--------------------+ + | +- *.rst | + +---------------------------------------+ + +files + + README this file + CMakeLists.txt build entry point + conf.py sphinx config + _static static files for sphinx + +map + + index.rst + +- install.rst + +- examples.rst + +- unit-quantities.rst + +- classes.rst + +- glossary.rst + ... + +examples + +.. doxygenclass:: ${c++ class name} + :project: + :path: + :members: + :protected-members: + :private-members: + :undoc-members: + :member-groups: + :members-only: + :outline: + :no-link: + :allow-dot-graphs: + +.. doxygendefine:: ${c preprocessor define} + +.. doxygenconcept:: ${c++ concept definition} + +.. doxygenenum:: ${c++ enum definition} + +.. doxygenfunction:: ${c++ function name} diff --git a/xo-unit/docs/_static/README b/xo-unit/docs/_static/README new file mode 100644 index 00000000..8230095c --- /dev/null +++ b/xo-unit/docs/_static/README @@ -0,0 +1 @@ +add any static {.html, .js, ..} files for sphinx to pickup here \ No newline at end of file diff --git a/xo-unit/docs/_static/img/favicon.ico b/xo-unit/docs/_static/img/favicon.ico new file mode 100644 index 00000000..15da2145 Binary files /dev/null and b/xo-unit/docs/_static/img/favicon.ico differ diff --git a/xo-unit/docs/basis-unit-class.rst b/xo-unit/docs/basis-unit-class.rst new file mode 100644 index 00000000..cf809a94 --- /dev/null +++ b/xo-unit/docs/basis-unit-class.rst @@ -0,0 +1,76 @@ +.. _basis-unit-class: + +Basis Unit +========== + +A unit representing a fixed multiple of a native dimension. + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + |cBLU basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +A :code:`basis_unit` represents a unit belonging to a single native dimension. +For example :code:`bu::meter` representing a distance of 1 meter. + +.. code-block:: cpp + + #include + +.. uml:: + :scale: 99% + :align: center + :caption: basis unit representing 1 minute + + object bu1<> + bu1 : native_dim = time + bu1 : scalefactor = 60 + +:code:`basis_unit` is intended as an implementation-level abstraction. +Application code will normally interact with the more-capable :code:`scaled_unit` +instead of :code:`basis_unit`. + +Class +----- + +.. doxygenclass:: xo::qty::basis_unit + +Member Variables +---------------- + +.. doxygengroup:: basis-unit-instance-vars + +Constructors +------------ + +.. doxygengroup:: basis-unit-constructors + +Access Methods +-------------- + +.. doxygengroup:: basis-unit-access-methods + +Comparison +---------- + +.. doxygengroup:: basis-unit-comparison-support diff --git a/xo-unit/docs/basis-unit-constants.rst b/xo-unit/docs/basis-unit-constants.rst new file mode 100644 index 00000000..caa833f9 --- /dev/null +++ b/xo-unit/docs/basis-unit-constants.rst @@ -0,0 +1,60 @@ +.. _basis-unit-constants: + +Basis Unit Constants +==================== + +Relative scalefactors for each built-in unit. + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + |cBLU basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +Constants in the :code:`xo::qty::detail::bu` namespace represent +low-level building blocks for specifying units. +Relative scalefactors for each unit are chosen here. + +Application code will not typically use these values directtly; +instead it's expected to use units from the :code:`xo::qty::u` namespace. +Those units are implemented on top of the basis units described here. + +.. code-block:: cpp + + #include + + using xo::qty::detail::bu; + +Mass Units +---------- + +.. doxygengroup:: basis-unit-mass-units + +Distance Units +-------------- + +.. doxygengroup:: basis-unit-distance-units + +Time Units +---------- + +.. doxygengroup:: basis-unit-time-units diff --git a/xo-unit/docs/basis-unit-reference.rst b/xo-unit/docs/basis-unit-reference.rst new file mode 100644 index 00000000..6ee84ccd --- /dev/null +++ b/xo-unit/docs/basis-unit-reference.rst @@ -0,0 +1,31 @@ +.. _basis-unit-reference: + +Basis Unit Reference +==================== + +Built-in named units for each native dimension + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + |cBLU basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. toctree:: + :maxdepth: 1 + + Basis Unit Class + basis-unit-constants diff --git a/xo-unit/docs/bpu-class.rst b/xo-unit/docs/bpu-class.rst new file mode 100644 index 00000000..eb8c6ae8 --- /dev/null +++ b/xo-unit/docs/bpu-class.rst @@ -0,0 +1,98 @@ +.. _bpu-class: + +BPU +=== + +A rational (usually integral) power of a single basis unit + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + |cBLU bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +.. code-block:: cpp + + #include + +A :code:`xo::qty::bpu` (aka "basis power unit") represents a rational (usually integer) power of a :doc:`basis-unit-class`. + +For example: + +.. uml:: + :caption: representation for cubic millimeters + :scale: 99% + :align: center + + object mm3<> + mm3 : native_dim = dim::distance + mm3 : scalefactor = 1/1000 + mm3 : power = 3/1 + +.. uml:: + :caption: representation for annual (365-day) volatility + :scale: 99% + :align: center + + object vol<> + vol : native_dim = dim::time + vol : scalefactor = 365*24*3600 + vol : power = -1/2 + +:code:`bpu` is intended as an implementation-level abstraction. +Application code will normally interact with the more-general :code:`scaled_unit` +instead of :code:`bpu`. + +Class +----- + +.. doxygenclass:: xo::qty::bpu + +Member Variables +---------------- + +.. doxygengroup:: bpu-instance-vars + +Constructors +------------ + +.. doxygengroup:: bpu-ctors + +Access Methods +-------------- + +.. doxygengroup:: bpu-access-methods + +Other Methods +------------- + +.. doxygengroup:: bpu-methods + +Comparison +---------- + +.. doxygengroup:: bpu-comparison + +Details +------- + +.. doxygengroup:: bpu-abbrev-helpers diff --git a/xo-unit/docs/bu-store-class.rst b/xo-unit/docs/bu-store-class.rst new file mode 100644 index 00000000..92283309 --- /dev/null +++ b/xo-unit/docs/bu-store-class.rst @@ -0,0 +1,129 @@ +.. _bu-store-class: + +Basis Unit Store +================ + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + |cBLU bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. code-block:: cpp + + #include + + namespace bu = xo::qty::detail::bu; + +A :code:`xo::qty::bu_store` is a small, constexpr, key-value store associating +abbreviations with basis units. To satisfy the constexpr requirement, +all unit abbreviations are irrevocably established from ``bu_store``'s constructor. + +The constant ``bu_abbrev_store`` contains a single instance of ``bu_store``, +recording all built-in units along with their associated abbreviations + +.. uml:: + :caption: basis-unit store + :scale: 99% + :align: center + + map mass_table<> { + bu::milligram => "mg" + bu::gram => "g" + bu::kilogram => "kg" + } + + map distance_table<> { + bu::millimeter => "mm" + bu::meter => "m" + bu::kilometer => "km" + } + + map time_table<> { + bu::millisecond => "ms" + bu::second => "s" + bu::minute => "min" + bu::hour => "hr" + } + + object bu_abbrev_store<> + bu_abbrev_store : bu_abbrev_vv[dim::mass] = mass_table + bu_abbrev_store : bu_abbrev_vv[dim::distance] = distance_table + bu_abbrev_store : bu_abbrev_vv[dim::time] = time_table + + bu_abbrev_store o-- mass_table + bu_abbrev_store o-- distance_table + bu_abbrev_store o-- time_table + + +This class exists to support the implementation of ``natural_unit::abbrev()``. + +Application code is not expected to interact directly with it. + +Class +----- + +.. doxygenclass:: xo::qty::detail::bu_store + +For example, this would be possible: + +.. code-block:: cpp + + #include + + namespace bu = using xo::qty::detail::bu; + using xo::qty::detail::bu_store; + using xo::qty::dim; + using xo::flatstring; + + constexpr bu_store store; + static_assert(store.bu_abbrev(bu::minute) == flatstring("min")); + static_assert(store.bu_abbrev(bu::microgram) == flatstring("ug")); + +.. doxygengroup:: bu-store-constructors +.. doxygengroup:: bu-store-access-methods +.. doxygengroup:: bu-store-implementation-methods + +.. doxygenclass:: xo::qty::detail::bu_dim_store +.. doxygengroup:: bu-dim-store-type-traits +.. doxygengroup:: bu-dim-store-instance-vars + + +Constants +--------- + +Provides dictionary of unit abbreviations + +Application code is not expected to interact directly with ``bu_abbrev_store``. + +.. doxygenvariable:: xo::qty::bu_abbrev_store + +Functions +--------- + +.. doxygenfunction:: xo::qty::bu_abbrev + +For example: + +.. code-block:: cpp + + #include + + namespace bu = xo::qty::bu; + using xo::qty::bu_abbrev; + using xo::flatstring; + + static_assert(bu_abbrev(bu::kilogram) == xo::flatstring("kg")); diff --git a/xo-unit/docs/conf.py b/xo-unit/docs/conf.py new file mode 100644 index 00000000..533aecf3 --- /dev/null +++ b/xo-unit/docs/conf.py @@ -0,0 +1,39 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'xo unit documentation' +copyright = '2024, Roland Conybeare' +author = 'Roland Conybeare' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +#extensions = [] +extensions = [ "breathe", + "sphinx.ext.mathjax", # inline math + "sphinx.ext.autodoc", # generate info from docstrings + "sphinxcontrib.ditaa", # diagrams-through-ascii-art + "sphinxcontrib.plantuml" # text -> uml diagrams + ] + +# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in +# match project name in Doxyfile.in +breathe_default_project = "xodoxxml" + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +pygments_style = 'sphinx' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_favicon = '_static/img/favicon.ico' diff --git a/xo-unit/docs/development.rst b/xo-unit/docs/development.rst new file mode 100644 index 00000000..d9695af4 --- /dev/null +++ b/xo-unit/docs/development.rst @@ -0,0 +1,18 @@ +.. _development: + +Development +=========== + +Miscellaneous development notes for *xo-unit*. + +Addding a Basis Unit +-------------------- + +To add a basis unit for an existing dimension: + +#. add unit definition to the ``xo::qty::bu`` namespace in ``include/xo/unit/basis_unit.hpp`` +#. add call to ``bu_store::bu_establish_abbrev()`` from ``bu_store::bu_store``. +#. add ``natural_unit`` definition to ``xo::qty::nu`` namespace in ``include/xo/unit/natural_unit.hpp`` +#. add ``scaled_unit`` definition to ``xo::qty::u`` namespace in ``include/xo/unit/scaled_unit.hpp``. +#. add unit quantity to ``xo::qty::qty`` namespace in ``include/xo/unit/quantity.hpp`` +#. add factory function to ``xo::qty::qty`` namespace in ``include/xo/unit/quantity.hpp`` diff --git a/xo-unit/docs/dimension-enum.rst b/xo-unit/docs/dimension-enum.rst new file mode 100644 index 00000000..863c38d1 --- /dev/null +++ b/xo-unit/docs/dimension-enum.rst @@ -0,0 +1,66 @@ +.. _dimension: + +Native Dimension +================ + +An abstract dimension; distinct native dimensions are orthogonal + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + |cBLU dimension | + +--------------------------------+ + +Introduction +------------ + +Identifies an abstract dimension, for example *mass* or *time*. + +.. code-block:: cpp + + #include + +For example can use this enum to index basis members of a :doc:`scaled_unit` instance: + +.. code-block:: cpp + :emphasize-lines: 7-8 + + #include + + using namespace xo::qty; + + auto x = q::kilometers(100) / q::hours(1); + + auto bpu1 = x.lookup_dim(dim::time); + auto bpu2 = x.lookup_dim(dim::distance); + +Enum +---- + +.. doxygenenum:: xo::qty::dimension + +Constants +--------- + +.. doxygenvariable:: xo::qty::n_dim + +Functions +--------- + +.. doxygenfunction:: xo::qty::dim2str diff --git a/xo-unit/docs/examples.rst b/xo-unit/docs/examples.rst new file mode 100644 index 00000000..292defb2 --- /dev/null +++ b/xo-unit/docs/examples.rst @@ -0,0 +1,366 @@ +.. _examples: + +.. toctree + :maxdepth: 2 + +Examples +======== + +Compile-time unit inference +--------------------------- + +See ``xo-unit/examples/ex1`` for code below. + +Units propagate through familiar arithmetic expressions: + +.. code-block:: cpp + :linenos: + :emphasize-lines: 14-15 + + #include "xo/unit/quantity.hpp" + #include "xo/unit/quantity_iostream.hpp" + #include + + int + main () { + namespace q = xo::qty::qty; + namespace su = xo::qty::su; + using namespace std; + + constexpr auto t = q::minutes(2); + constexpr auto d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + cerr << "t: " << t << ", d: " << d + << ", t^2: " << t2 + << ", d.t^-2: " << a + << endl; + } + +with output: + +.. code-block:: + + t: 2min, d: 2.5km, t^2: 4min^2, d.t^-2: 0.625km.min^-2 + +We can use static asserts to prove that units are being computed at compile-time + +.. code-block:: cpp + :linenos: + + static_assert(std::same_as); + static_assert(sizeof(t) == sizeof(double)); + static_assert(t.scale() == 2); + static_assert(t.abbrev() == flatstring("min")); + + static_assert(std::same_as); + static_assert(sizeof(d) == sizeof(double)); + static_assert(d.scale() == 2.5); + static_assert(d.abbrev() == flatstring("km")); + + static_assert(std::same_as); + static_assert(sizeof(t2) == sizeof(double)); + static_assert(t2.scale() == 4); + static_assert(t2.abbrev() == flatstring("min^2")); + + static_assert(std::same_as); + static_assert(sizeof(a) == sizeof(double)); + static_assert(a.scale() == 0.625); + static_assert(a.abbrev() == flatstring("km.min^-2")); + +Remarks: + +- ``xo::qty::quantity`` performs unit consistency checking and propagation at compile time. Runtime space/time overhead is zero. +- Units are sticky: since we expressed ``t`` in minutes, ``(t*t)`` and ``d/(t*t)`` also use minutes. +- Unit ordering is sticky. Distance appears on the left of time in printed value of ``d/(t*t)`` + because it was on the left-hand side of ``operator/`` +- ``xo-unit`` copies representation from the argument to factory functions ``q::minutes``, ``q::kilometers`` etc. +- Binary operators take representation from the 'most precise' argument, as prescribed by ``std::common_type_t``. +- Unit abbreviations (such as ``kg.min^-2`` above) are computed at compile time. + See ``xo::flatstring`` for constexpr string implementation. +- See ``xo::xquantity`` for parallel implementation that defers unit checking until runtime. + + +Explicit scale conversion +------------------------- + +Can convert between compatible units explictly, +using: + +1. ``xo::qty::with_units`` (template function) +2. ``quantity.rescale_ext`` (template method) +3. ``xo::qty::with_units_from`` (template function) + +See ``xo-unit/examples/ex2`` for code below. + +.. code-block:: cpp + :linenos: + :emphasize-lines: 10,13,16-17 + + #include "xo/unit/quantity.hpp" + #include "xo/unit/quantity_iostream.hpp" + #include + + int + main () { + namespace q = xo::qty::qty; + namespace u = xo::qty::u; + using xo::qty::with_units_from; + using xo::qty::with_units; + using xo::qty::quantity; + using xo::flatstring; + using namespace std; + + constexpr auto t = q::minutes(2); + constexpr auto d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + cerr << "t: " << t << ", d: " << d + << ", t^2: " << t2 + << ", d.t^-2: " << a + << endl; + + constexpr auto a2 = with_units(a); + + static_assert(a2.abbrev() == flatstring("m.s^-2")); + + cerr << "a2: " << a2 << endl; + + constexpr auto a3 = a.rescale_ext(); + + static_assert(a3.abbrev() == flatstring("m.s^-2")); + + cerr << "a3: " << a3 << endl; + + constexpr auto au = q::meter / (q::second * q::second); + constexpr auto a4 = with_units_from(a, au); + + static_assert(a4.abbrev() == flatstring("m.s^-2")); + + cerr << "a4: " << a4 << endl; + } + +with output: + +.. code-block:: cpp + + a2: 0.173611m.s^-2 + a3: 0.173611m.s^-2 + a4: 0.173611m.s^-2 + +Implicit Scale conversion +------------------------- + +Another way to convert units is to assign to a variable +with desired units -- this works because the units are encoded +as part of the assigned variable's type. + +See ``xo-unit/example/ex3`` for code below + +.. code-block:: cpp + :linenos: + :emphasize-lines: 12-13 + + int + main () { + namespace q = xo::qty::qty; + namespace u = xo::qty::u; + using xo::qty::quantity; + + constexpr quantity t = q::minutes(2); + constexpr quantity d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + std::cerr << "t: " << t << ", d: " << d + << ", d.t^-2: " << a + << std::endl; + } + +with output: + +.. code-block:: + + t: 120s, d: 2500m, d.t^-2: 0.17e611m.s^-2 + +Remarks: + +- Assignment to ``t`` converted to representation ``double``. + We could have instead used :code:`quantity` to propagate + right-hand-side representation + +Scale conversion and arithmetic +------------------------------- + +When representing a particular quantity, +xo-unit uses at most one scale for each :term:`basis dimension` associated with the unit for that quantity. +When an arithmetic operator encounters basis units involving two different scales, +the operator will adopt the scale provided by the left-hand argument: + +See ``xo-unit/example/ex4`` for code below + +.. code-block:: cpp + :linenos: + :emphasize-lines: 11 + + #include "xo/unit/quantity.hpp" + #include + + int main() { + namespace q = xo::qty::qty; + + auto t1 = qty::milliseconds(1); + auto t2 = qty::minutes(1); + auto p = t1 * t2; + + std::cerr << "t1: " << t1 << ", t2: " << t2 << ", p: " << p << std::endl; + } + +with output: + +.. code-block:: + + t1: 1ms, t2: 1min, t1*t2: 60000ms^2 + + +Dimensionless quantities unwrap implicitly +------------------------------------------ + +Conversely, compiler rejects attempt to implictly unwrap a dimensioned quantity. + +See ``xo-unit/examples/ex4`` for code below. + +.. code-block:: cpp + :linenos: + :emphasize-lines: 23,26 + + #include "xo/unit/quantity.hpp" + #include "xo/unit/quantity_iostream.hpp" + #include + + int + main () { + namespace q = xo::qty::qty; + + auto t1 = q::milliseconds(1); + auto t2 = q::minutes(1); + + auto r1 = t1 / with_repr(t2); + + static_assert(r1.is_dimensionless()); + static_assert(!t2.is_dimensionless()); + + static_assert(std::same_as(r1), double>); + + // r1_value: assignment compiles, since r1 dimensionless + double r1_value = r1; + + // r2_value: bad assignment won't compile, 'cannot convert' error + //double r2_value = t2; + + std::cerr << "t1: " << t1 << ", t2: " << t2 << ", t1/t2: " << r1_value << std::endl; + } + +with output: + +.. code-block:: + + t1: 1ms, t2: 1min, t1/t2: 1.66667e-05 + + +Fractional dimension +-------------------- + +Fractional dimensions have limited support. +Prior to c++26 we can only support fractional dimensions with denominator 2, +such as powers -3/2, -1/2, +1/2, +3/2 etc. + +c++26 will enable support for support fractional dimensions involving other ratios, +by offering constexpr ``::pow()`` + +See ``xo-unit/examples/ex6`` for code below + +.. code-block:: cpp + :linenos: + :emphasize-lines: 15 + + #include "xo/unit/quantity.hpp" + #include "xo/unit/quantity_iostream.hpp" + #include + + int + main () { + namespace u = xo::unit::units; + namespace q = xo::unit::qty; + using namespace std; + + /* 20% volatility over 250 days (approx number of trading days in one year) */ + auto q1 = q::volatility_250d(0.2); + /* 10% volatility over 30 days */ + auto q2 = q::volatility_30d(0.1); + + auto sum = q1 + q2; + auto prod = q1 * q2; + + static_assert(sum.abbrev() == flatstring("yr360^(-1/2)")); + static_assert(prod.abbrev() == flatstring("yr360^-1")); + + std::cerr << "q1: " << q1 << std::endl; + std::cerr << "q2: " << q2 << std::endl; + std::cerr << "q1+q2: " << sum << std::endl; + std::cerr << "q1*q2: " << prod << std::endl; + } + +with output: + +.. code-block:: + + q1: 0.2yr360^(-1/2) + q2: 0.1mo^(-1/2) + q1+q2: 0.54641yr360^(-1/2) + q1*q2: 0.069282yr360^-1 + + +Dynamic dimension +----------------- + +If the dimension (or units) associated with a quantity are not known at compile-time, +use ``xo::qty::xquantity`` instead of ``xo::qty::quantity``. + +See ``xo-unit/example/ex8`` for code below + +.. code-block:: cpp + :linenos: + :emphasize-lines: 10-12 + + #include "xo/unit/xquantity.hpp" + #include "xo/unit/xquantity_iostream.hpp" + #include + + int + main () { + using namespace xo::qty; + namespace u = xo::qty::u; + + xquantity qty1(7, u::foot); + xquantity qty2(6.0, u::inch); + xquantity qty3 = qty1 + qty2; + + std::cerr << "qty1: " << qty1 << std::endl; + std::cerr << "qty2: " << qty2 << std::endl; + std::cerr << "qty3: " << qty3 << std::endl; + + /* rescale to mm */ + xquantity res = qty3.rescale(xo::qty::nu::millimeter); + + /* 2286mm */ + std::cerr << "res: " << res << std::endl; + } + +Here ``u::foot`` and ``u::inch`` are literals, +but they could have been read from console input or another runtime-only context. diff --git a/xo-unit/docs/glossary.rst b/xo-unit/docs/glossary.rst new file mode 100644 index 00000000..51895cc9 --- /dev/null +++ b/xo-unit/docs/glossary.rst @@ -0,0 +1,43 @@ +.. _glossary: + +Glossary +-------- + +.. glossary:: + dimension + dim + | Fundamental, orthogonal directions associated available for constructing units. + | For example *mass*, *length*, *time*. + | In *xo-unit* these are represented by the enum :doc:`xo::qty::dimension`. + + basis unit + bu + | An implementation type representing a quantity (with associated scale) in the direction of a single :term:`dimension`. + | For example *milliseconds*, *seconds*, and *hours* stand for different basis units within the *time* dimension. + | In *xo-unit* these are represented by the template type :doc:`xo::qty::basis_unit`. + + basis power unit + bpu + | A rational power of a (single) basis unit. + | For example :math:`s^{-2}` for unit "per second squared" or :math:`yr^{-(1/2)}` for "per square-root of a year". + | In *xo-unit* these are represented by the template type :doc:`xo::qty::bpu` + + natural unit + nu + | A cartesian product of basis power units (bpus); + | For example :math:`kg.m.s^{-2}` or :math:`hr^{-(1/2)}`. + | In *xo-unit* these are represented by template type :doc:`xo::qty::natural_unit`. + + scaled unit + su + | A dimensionless multiple of a natural unit. + | Used as intermediate value when coalescing quantities involving different units. + | In *xo-unit* these are represented by template type :doc:`xo::qty::scaled_unit`. + + XO + A set of integrated c++ libraries for complex event processing, with browser and python integration. + `xo documentation here`_ + +.. _xo documentation here: https://rconybea.github.io/web/sw/xo.html + +.. toctree:: diff --git a/xo-unit/docs/implementation.rst b/xo-unit/docs/implementation.rst new file mode 100644 index 00000000..86ec396b --- /dev/null +++ b/xo-unit/docs/implementation.rst @@ -0,0 +1,314 @@ +.. _implementation: + +Components +========== + +Library dependency tower for *xo-unit*: + +.. ditaa:: + + +-----------------+ + | xo_unit | + +-----------------+ + | xo_ratio | + +-----------------+ + | xo_flatstring | + +-----------------+ + | xo_cmake | + +-----------------+ + +Install instructions :doc:`here` + +Abstraction tower for *xo-unit* components: + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +- :doc:`quantity`: + + A quantity with unit checking and conversion done at compile-time + + .. code-block:: cpp + + #include "xo/unit/quantity.hpp" + auto q1 = xo::qty::qty::kilometers(7.5); + +- :doc:`xquantity`: + + A quantity with unit checking and conversion done at run-time. + This is useful if unit information isn't known at compile time, for example + if reading units from console input. + + .. code-block:: cpp + + #include "xo/unit/xquantity.hpp" + xquantity qty1(7.5, xo::qty::u::foot) + + +- :doc:`scaled_unit`: + + .. code-block:: cpp + + #include "xo/unit/scaled_unit.hpp" + auto u = xo::qty::u::millimeter; + + A unit involving zero or more dimensions, and associated conversion factor. + + - can express result of arithmetic involving multiple scales, + by reporting an outer scalefactor + - a scaled unit is 'natural' if its outer scalefactor is 1. + - quantities are represented by associating a natural scaled_unit instance + - scaled_units are closed under multiplication and division. + - multiplication and division commit to a single :code:`basis_unit` for each + dimension. + +- :doc:`natural_unit` + + .. code-block:: cpp + + #include "xo/unit/natural_unit.hpp" + auto u = xo::qty::nu::millimeter; + + A unit involving zero or more dimensions, and at most one scale per dimension. + A quantity instance is always represented as a dimensionless multiple + of a natural unit + + - natural_units are *not* closed under multiplication and division. + (for example consider :code:`xo::qty::qty::foot * xo::qty::qty::meter`) + +- :doc:`bpu` + + A rational (usually integer) power of a basis unit. Has a single dimension. + + .. code-block:: cpp + + #include "xo/unit/bpu.hpp" + xo::qty::bpu(xo::qty::detail::bu::millimeter, + xo::qty::power_ratio_type(2)); // mm^2 + +- :doc:`bu_store` + + Associates basis units with abbreviations. + Abbreviations used to decorate printed quantities. + + For example ``bu::kilogram`` => ``"kg"`` + + .. code-block:: cpp + + #include "xo/unit/bu_store.hpp" + xo::qty::bu_abbrev_store.bu_abbrev(xo::qty::detail::bu::picogram); // "pg" + +- :doc:`basis_unit` + + A unit with a single dimension and scale. + + .. code-block:: cpp + + #include "xo/unit/basis_unit.hpp" + auto b = xo::qty::detail::bu::picogram; + +- :doc:`dimension` + + identifies a dimension, such as mass or time. + + .. code-block:: cpp + + #include "xo/unit/dimension.hpp" + auto d = xo::qty::dimension::mass; + +Representation +============== + +Worked example using :cpp:class:`xo::qty::quantity`. + +.. code-block:: cpp + :linenos: + :emphasize-lines: 6 + + #include "xo/unit/quantity.hpp" + ... + using xo::qty; + namespace q = xo::qty::qty; + + // 7.55km.min^-2 + quantity qty1 = 7.55 * q::kilometer / (q::minute * q::minute); + +Note: in diagrams below, components with pale blue background are discarded before runtime + +.. uml:: + :caption: representation for quantity 7.55km.min^-2 + :scale: 99% + :align: center + + object qty1<> + qty1 : scale = 7.55 + + rectangle #e0f0ff { + + object km_per_min2<> + km_per_min2 : n_bpu = 2 + km_per_min2 : bpu[0] = km + km_per_min2 : bpu[1] = per_min + + object km<> + km : native_dim = dim.mass + km : scalefactor = 1000/1 + km : power = 1 + + object per_min2<> + per_min2 : native_dim = dim.time + per_min2 : scalefactor = 60/1 + per_min2 : power = -2 + + qty1 o-- km_per_min2 : s_unit (static constexpr) + + km_per_min2 *-- km + km_per_min2 *-- per_min2 + + } + +.. code-block:: cpp + :linenos: + + // 123ng + quantity qty2 = q::nanograms(123); + +.. uml:: + :caption: representation for quantity 123 nanograms + :scale: 99% + :align: center + + object qty2<> + qty2 : scale = 123 + + rectangle #e0f0ff { + + object ng_unit<> + ng_unit : n_bpu = 1 + ng_unit : bpu[0] = ng + + object ng<> + ng : native_dim = dim::mass + ng : scalefactor = 1/10^9 + ng : power = 1 + + qty2 o-- ng_unit : s_unit (static constexpr) + + ng_unit *-- ng + + } + +.. code-block:: cpp + :linenos: + + // (123*7.55) ng.km.min^-2 + quantity qty3 = qty2 * qty1; + +.. uml:: + :caption: quantity 928.65 ng.km.min^-2 + :scale: 99% + :align: center + + object qty3<> + qty3 : scale = 928.65 + + rectangle #e0f0ff { + + object ng_km_min2_unit<> + ng_km_min2_unit : n_bpu = 3 + ng_km_min2_unit : bpu[0] = ng + ng_km_min2_unit : bpu[1] = km + ng_km_min2_unit : bpu[2] = per_min2 + + object ng<> + ng : native_dim = dim::mass + ng : scalefactor = 1/10^9 + ng : power = 1 + + object km<> + km : native_dim = dim::distance + km : scalefactor = 1000/1 + km : power = 1 + + object per_min2<> + per_min2 : native_dim = dim::time + per_min2 : scalefactor = 60/1 + per_min2 : power = -2 + + qty3 o-- ng_km_min2_unit : s_unit (static constexpr) + + ng_km_min2_unit *-- ng + ng_km_min2_unit *-- km + ng_km_min2_unit *-- per_min2 + } + +.. code-block:: cpp + :linenos: + + namespace u = xo::qty::u; + + // (123*7.55) ng.km.min^-2 ==> 2.57958e-10kg.m.s^-2 + + constexpr auto newton = u::kilogram * u::meter / (u::second * u::second); + + quantity qty3b = qty3; + + // quantity qty3b = qty3.rescale_ext(); + +.. uml:: + :caption: quantity 928.65 ng.km.min^-2 + :scale: 99% + :align: center + + object qty3b<> + qty3b : scale = 2.59758e-10 + + rectangle #e0f0ff { + + object kg_m_s2_unit<> + kg_m_s2_unit : n_bpu = 3 + kg_m_s2_unit : bpu[0] = kg + kg_m_s2_unit : bpu[1] = m + kg_m_s2_unit : bpu[2] = per_s2 + + object kg<> + kg : native_dim = dim::mass + kg : scalefactor = 1000/1 + kg : power = 1 + + object m<> + m : native_dim = dim::distance + m : scalefactor = 1/1 + m : power = 1 + + object per_s2<> + per_s2 : native_dim = dim::time + per_s2 : scalefactor = 1/1 + per_s2 : power = -2 + + qty3b o-- kg_m_s2_unit : s_unit (static constexpr) + + kg_m_s2_unit *-- kg + kg_m_s2_unit *-- m + kg_m_s2_unit *-- per_s2 + } + +.. toctree:: + :maxdepth: 2 + :caption: Abstractions diff --git a/xo-unit/docs/index.rst b/xo-unit/docs/index.rst new file mode 100644 index 00000000..86ae0e63 --- /dev/null +++ b/xo-unit/docs/index.rst @@ -0,0 +1,48 @@ +.. xo-unit-examples documentation master file, created by + sphinx-quickstart on Wed Mar 6 23:32:27 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +xo-unit documentation +===================== + +xo-unit is a lightweight header-only library that provides compile-time +dimension checking and unit conversion. + +Functionality is similar in spirit to that provided by ``boost::units``; +however there are some important differences: + +* streamlined implementation using c++20 features. +* along with no-runtime-overhead compile-time unit inference, + also provides defer-until-runtime representation. +* constexpr string representation for things like unit abbreviations. +* supports fractional dimensions, for concepts like volatility. +* integration with python (see sister project xo-pyunit) + +Second, ``xo-unit`` supports fractional dimensions. This allows using it to naturally handle +concepts like volatility (dimension 1/sqrt(time)), for example. + +Finally, ``xo-unit`` is written with the expectation of providing +python integration via pybind11. This requires a parallel set of data structures that can work at +runtime (since we can't construct new c++ types at runtime). + +.. toctree:: + :maxdepth: 2 + :caption: xo-unit contents + + install + examples + implementation + quantity-reference + xquantity-reference + scaled-unit-reference + natural-unit-class + bpu-class + bu-store-class + basis-unit-reference + dimension-enum + development + ubuntu-github-workflow + glossary + genindex + search diff --git a/xo-unit/docs/install.rst b/xo-unit/docs/install.rst new file mode 100644 index 00000000..1bab499b --- /dev/null +++ b/xo-unit/docs/install.rst @@ -0,0 +1,305 @@ +.. _install: + +.. toctree + :maxdepth: 2 + +Install +======= + +``xo-unit`` uses supporting header-only libraries ``xo-ratio`` and ``xo-flatstring``. +and (optionally) cmake macros ``xo-cmake``. These are on github: + +- `xo-unit source`_ (constexpr quantities, units and dimension-checking) +- `xo-ratio source`_ (constexpr exact ratios) +- `xo-flatstring source`_ (constexpr strings) +- `xo-cmake source`_ (shared cmake macros) + +.. _xo-unit source: https://github.com/rconybea/xo-unit +.. _xo-ratio source: https://github.com/rconybea/xo-ratio +.. _xo-flatstring source: https://github.com/rconybea/xo-flatstring +.. _xo-cmake source: https://github.com/rconybea/xo-cmake + +`xo-cmake` is nccessary to invoke `xo` cmake build for: + +- site install +- example programs +- unit tests + +Can omit to instead copy `xo_unit`, `xo-ratio` and `xo-flatstring` source trees; +see instructions below for including as git submodule + +Implementation relies on some c++20 features (for example class-instances as template arguments). +Tested with gcc 12.3, 13.2 + +Include as submodule +-------------------- + +One way to use ``xo-unit`` in a project is to import the source tree directly: + +.. code-block:: bash + + cd myproject + git submodule add -b main https://github.com/rconybea/xo-flatstring ext/xo-flatstring + git submodule add -b main https://github.com/rconybea/xo-ratio ext/xo-ratio + git submodule add -b main https://github.com/rconybea/xo-unit ext/xo-unit + git submodule update --init + +This assumes you organize directly-incorporated dependencies under directory ``myproject/ext``. +You would then add to your compiler's include path the directories ``myproject/ext/xo-flatstring/include``, +``myproject/ext/xo-ratio/include``, ``myproject/ext/xo-unit/include``; + +and add + +.. code-block:: c++ + + #include + +to c++ source files that rely on xo-unit + +Ubuntu Install Pattern +---------------------- + +:doc:`Example instructions` (used for github actions CI) for build starting from stock ubuntu + +Installing from source +---------------------- + +Install scripts for `xo-unit`, `xo-ratio` and `xo-flatstring` depend on shared cmake macros +and a bootstrap script `xo-cmake-config` from `xo-cmake`. + +Preamble: + +.. code-block:: bash + + mkdir -p ~/proj/xo + cd ~/proj/xo + + git clone https://github.com/rconybea/xo-cmake + git clone https://github.com/rconybea/xo-flatstring + git clone https://github.com/rconybea/xo-ratio + git clone https://github.com/rconybea/xo-unit + + PREFIX=~/local # ..or other desired installation prefix + +Ordering below is important; cmake support for each subsystem +requires successful installation of its predecessor. + +Install `xo-cmake`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-cmake/.build -S xo-cmake + cmake --build xo-cmake/.build -j # placeholder, no-op for now + cmake --install xo-cmake/.build + +Build and Install `xo-flatstring`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DENABLE_TESTING=0 \ + -B xo-flatstring/.build -S xo-flatstring + cmake --build xo-flatstring/.build -j + cmake --install xo-flatstring/.build + +Build and Install `xo-ratio`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DENABLE_TESTING=0 \ + -B xo-ratio/.build -S xo-ratio + cmake --build xo-ratio/.build -j + cmake --install xo-ratio/.build + +Build and Install `xo-unit`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DENABLE_TESTING=0 \ + -B xo-unit/.build -S xo-unit + cmake --build xo-unit/.build -j + cmake --install xo-unit/.build + +Directories under ``PREFIX`` will then contain: + +.. code-block:: + + PREFIX + +- bin + | \- xo-cmake-config + +- include + | \- xo + | +- cxxutil/ + | +- flatstring/ + | +- ratio/ + | +- unit/ + +- lib + | \- cmake + | +- indentlog/ + | +- randomgen/ + | +- xo_flatstring/ + | \- xo_unit/ + +- share + \- cmake + \- xo_macros/ + +Use CMake Support +----------------- + +To use built-in cmake suport: + +Make sure ``PREFIX/lib/cmake`` is searched by cmake (if necessary, include it in ``CMAKE_PREFIX_PATH``) + +Add to ``CMakeLists.txt``: + +.. code-block:: cmake + + FindPackage(xo_unit CONFIG REQUIRED) + + target_link_libraries(mytarget INTERFACE xo_unit) + +Build and Install with Unit Tests Enabled +----------------------------------------- + +Running unit tests require a few additional dependencies: + +* `catch2`_ header-only unit-test framework +* `xo-indentlog`_ logging with call-structure indenting +* `xo-randomgen`_ fast random number generator (xoshiro256ss) + +.. _catch2: https://github.com/catchorg/Catch2 +.. _xo-indentlog: https://github.com/rconybea/indentlog +.. _xo-randomgen: https://github.com/rconybea/randomgen + +Preamble: + +.. code-block:: bash + + mkdir -p ~/proj/xo + cd ~/proj/xo + + git clone https://github.com/rconybea/xo-cmake + git clone https://github.com/rconybea/indentlog xo-indentlog + git clone https://github.com/rconybea/randomgen xo-randomgen + git clone https://github.com/rconybea/xo-flatstring + git clone https://github.com/rconybea/xo-ratio + git clone https://github.com/rconybea/xo-unit + + PREFIX=~/local # ..or other desired installation prefix + +Build and Install `catch2` (assuming ubuntu here): + +.. code-block:: bash + + sudo apt-get install catch2 # on ubuntu, for example + +Build and Install `xo-cmake`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-cmake/.build -S xo-cmake + cmake --build xo-cmake/.build -j # placeholder, no-op for now + cmake --install xo-cmake/.build + +Build, Test and Install `xo-indentlog`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-indentlog/.build -S xo-indentlog + cmake --build xo-indentlog/.build -j + cmake --build xo-indentlog/.build -- test # run unit tests, cmake invokes ctest + (cd xo-indentlog/.build && ctest) # or invoke ctest directly + cmake --install xo-indentlog/.build + +Build and Install `xo-randomgen` (no unit tests yet): + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-randomgen/.build -S xo-randomgen + cmake --build xo-randomgen/.build -j + cmake --install xo-randomgen/.build + +Build, Test and Install `xo-flatstring`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-flatstring/.build -S xo-flatstring + cmake --build xo-flatstring/.build -j + cmake --build xo-flatstring/.build -- test # run unit tests, cmake invokes ctest + (cd xo-flatstring/.build && ctest) # or invoke ctest directly + cmake --install xo-flatstring/.build + +Build, Test and Install `xo-ratio`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-ratio/.build -S xo-ratio + cmake --build xo-ratio/.build -j + cmake --build xo-ratio/.build -- test # run unit tests, cmake invokes ctest + (cd xo-ratio/.build && ctest) # or invoke ctest directly + cmake --install xo-ratio/.build + +Build, Test and Install `xo-unit`: + +.. code-block:: bash + + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-unit/.build -S xo-unit + cmake --build xo-unit/.build -j + cmake --build xo-unit/.build -- test # run unit tests, cmake invokes ctest + (cd xo-unit/.build && ctest) # or invoke ctest directly + cmake --install xo-unit/.build + +Build Examples +-------------- + +To enable building example programs: + +.. code-block:: bash + + cd ~/proj/xo + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -DXO_ENABLE_EXAMPLES=1 -B xo-unit/.build -S xo-unit + +Run examples from the build directory: + +.. code-block:: bash + + ~/proj/xo/xo-unit/.build/example/ex1/xo_unit_ex1 + ~/proj/xo/xo-unit/.build/example/ex2/xo_unit_ex2 + # etc + +Build and Install Documentation +------------------------------- + +xo-unit documentation has these additional dependencies: + +* `doxygen`_ annotation-driven inline documentation +* `sphinx`_ documentation based on ReST files +* `sphinx-rtd-theme`_ popular CSS theme for sphinx +* `breathe`_ make doxygen-generated ingredients available from sphinx + +.. _doxygen: https://www.doxygen.nl +.. _sphinx: https://www.sphinx-doc.org +.. _sphinx-rtd-theme: https://pypi.org/project/sphinx-rtd-theme +.. _breathe: https://breathe.readthedocs.io/en/latest + +Preamble (assuming ubuntu here): + +.. code-block:: bash + + sudo apt-get install doxygen + sudo apt-get install python3-sphinx + sudo apt-get install python3-sphinx-rtd-theme + sudo apt-get install python3-breathe + +Build `xo-unit` docs + +.. code-block:: bash + + cd ~/proj/xo + cmake -DCMAKE_INSTALL_PREFIX=$PREFIX -B xo-unit/.build + cmake --build xo-unit/.build -- docs + cmake --install xo-unit/.build # if docs built, installs to $PREFIX/share/doc/xo_unit/html + +Supported compilers +------------------- + +* developed with gcc 12.3.0 and gcc 13.2.0; github CI using gcc 11.4.0 (asof March 2024) diff --git a/xo-unit/docs/natural-unit-class.rst b/xo-unit/docs/natural-unit-class.rst new file mode 100644 index 00000000..61504af1 --- /dev/null +++ b/xo-unit/docs/natural-unit-class.rst @@ -0,0 +1,109 @@ +.. _natural-unit-class: + +Natural Unit +============ + +A natural unit represents a product of terms, each involving a distinct basis dimension + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + |cBLU natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +.. code-block:: cpp + + #include + +Representation for the unit associated with a :doc:`xquantity` + +- represents a cartesian product of basis units. +- constexpr implementation +- limited support for fractional dimensions such as time^-1/2 + +.. uml:: + :caption: natural unit for a Newton (unit of force) + :scale: 99% + :align: center + + object newton<> + newton : n_bpu = 3 + newton : bpu_v[] + + object kg<> + kg : native_dim = dim::mass + kg : scalefactor = 1000/1 + kg : power = 1/1 + + object m<> + m : native_dim = dim::distance + m : scalefactor = 1/1 + m : power = 1/1 + + object s2<> + s2 : native_dim = dim::time + s2 : scalefactor = 1/1 + s2 : power = -2/1 + + newton o-- kg + newton o-- m + newton o-- s2 + +Class +----- + +.. doxygenclass:: xo::qty::natural_unit + +Member Variables +---------------- + +.. doxygengroup:: natural-unit-instance-vars + +Type Traits +----------- + +.. doxygengroup:: natural-unit-type-traits + +Constructors +------------ + +.. doxygengroup:: natural-unit-ctors + +Access Methods +-------------- + +.. doxygengroup:: natural-unit-access-methods + +General Methods +--------------- + +.. doxygengroup:: natural-unit-methods + +Conversion +---------- + +.. doxygengroup:: natural-unit-conversion-methods + +Comparison Functions +-------------------- + +.. doxygengroup:: natural-unit-comparison-functions diff --git a/xo-unit/docs/quantity-class.rst b/xo-unit/docs/quantity-class.rst new file mode 100644 index 00000000..2262c845 --- /dev/null +++ b/xo-unit/docs/quantity-class.rst @@ -0,0 +1,144 @@ +.. _quantity-class: + +Quantity +======== + +Dimensioned quantity with compile-time unit checking/conversion + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + |cBLU quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. code-block:: cpp + + #include + +.. uml:: + :scale: 99% + :align: center + + allowmixing + + object qty1<> + qty1 : scale = 1.23 + + rectangle constexpr #e0f0ff { + + object unit<> + unit : is_natural() = true + + qty1 o-- unit : s_scaled_unit (static constexpr) + + } + +- Arithmetic on :doc:`xo::qty::quantity` instances + does *not* use ``xo::qty::quantity::s_scaled_unit`` at runtime; + instead gets everything it needs at compile time. + +- The :code:`xo::qty::quantity` template takes a :doc:`xo::qty::scaled_unit` instance, + but only accepts values with :code:`xo::qty::scaled_unit::is_natural() == true`. + + This accomodation (instead of requiring a :doc:`xo::qty::natural_unit` instance + is to make possible code like this possible: + + .. code-block:: cpp + + #include "xo/unit/quantity.hpp" + + using namespace xo::qty; + + quantity x; + quantity y; + + while rejecting attempt to mix multiple scales in the same quantity value: + + .. code-block:: cpp + + quantity x; // will not compile + +Class +----- + +The primary data structure for interacting with xo-unit is the +template class ``xo::qty::quantity``. +A quantity is a compile-time wrapper around a single arithmetic value, +with type taken from the ``Repr`` parameter in ``quantity``. + +.. doxygenclass:: xo::qty::quantity + +Member Variables +---------------- + +.. doxygengroup:: quantity-static-vars +.. doxygengroup:: quantity-instance-vars + +Type Traits +----------- + +.. doxygengroup:: quantity-type-traits + +Constructors +------------ + +.. doxygengroup:: quantity-ctors + +The simplest way to create a quantity instance is to use either + +* factory functions in ``xo::qty::qty``, see :doc:`quantity-factoryfunctions` +* unit variables in ``xo::qty::qty``, see :doc:`quantity-unitvars` + +Assignment +---------- + +.. doxygengroup:: quantity-assignment + +Access Methods +-------------- + +.. doxygengroup:: quantity-access-methods + +Constants +--------- + +.. doxygengroup:: quantity-constants + +Conversion Methods +------------------ + +Amount-preserving conversion to quantities with different units and/or representation. + +.. doxygengroup:: quantity-unit-conversion + +Arithmetic +---------- + +.. doxygengroup:: quantity-operators + +Support methods for arithmetic operations + +.. doxygengroup:: quantity-arithmetic-support + +Comparison +---------- + +Support methods for comparison operators + +.. doxygengroup:: quantity-comparison-support diff --git a/xo-unit/docs/quantity-factoryfunctions.rst b/xo-unit/docs/quantity-factoryfunctions.rst new file mode 100644 index 00000000..a447d75e --- /dev/null +++ b/xo-unit/docs/quantity-factoryfunctions.rst @@ -0,0 +1,94 @@ +.. _quantity_factoryfunctions: + +Quantity Factory Functions +========================== + +Convenience functions for creating quantities with compile-time units + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + |cBLU quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. toctree:: + :maxdepth: 2 + +Introduction +------------ + +.. code-block:: cpp + + #include + +Mass +---- +.. doxygenfunction:: xo::qty::qty::picograms +.. doxygenfunction:: xo::qty::qty::nanograms +.. doxygenfunction:: xo::qty::qty::micrograms +.. doxygenfunction:: xo::qty::qty::milligrams +.. doxygenfunction:: xo::qty::qty::grams +.. doxygenfunction:: xo::qty::qty::kilograms +.. doxygenfunction:: xo::qty::qty::tonnes +.. doxygenfunction:: xo::qty::qty::kilotonnes +.. doxygenfunction:: xo::qty::qty::megatonnes +.. doxygenfunction:: xo::qty::qty::gigatonnes + +Distance +-------- +.. doxygenfunction:: xo::qty::qty::picometers +.. doxygenfunction:: xo::qty::qty::nanometers +.. doxygenfunction:: xo::qty::qty::micrometers +.. doxygenfunction:: xo::qty::qty::millimeters +.. doxygenfunction:: xo::qty::qty::meters +.. doxygenfunction:: xo::qty::qty::kilometers +.. doxygenfunction:: xo::qty::qty::megameters +.. doxygenfunction:: xo::qty::qty::gigameters + +.. doxygenfunction:: xo::qty::qty::lightseconds +.. doxygenfunction:: xo::qty::qty::astronomicalunits + +.. doxygenfunction:: xo::qty::qty::inches +.. doxygenfunction:: xo::qty::qty::feet +.. doxygenfunction:: xo::qty::qty::yards +.. doxygenfunction:: xo::qty::qty::miles + +Time +---- +.. doxygenfunction:: xo::qty::qty::picoseconds +.. doxygenfunction:: xo::qty::qty::nanoseconds +.. doxygenfunction:: xo::qty::qty::microseconds +.. doxygenfunction:: xo::qty::qty::milliseconds +.. doxygenfunction:: xo::qty::qty::seconds +.. doxygenfunction:: xo::qty::qty::minutes +.. doxygenfunction:: xo::qty::qty::hours +.. doxygenfunction:: xo::qty::qty::days +.. doxygenfunction:: xo::qty::qty::weeks +.. doxygenfunction:: xo::qty::qty::months +.. doxygenfunction:: xo::qty::qty::years +.. doxygenfunction:: xo::qty::qty::year250s +.. doxygenfunction:: xo::qty::qty::year360s +.. doxygenfunction:: xo::qty::qty::year365s + +Volatility +---------- +.. doxygenfunction:: xo::qty::qty::volatility_30d +.. doxygenfunction:: xo::qty::qty::volatility_250d +.. doxygenfunction:: xo::qty::qty::volatility_360d +.. doxygenfunction:: xo::qty::qty::volatility_365d diff --git a/xo-unit/docs/quantity-reference.rst b/xo-unit/docs/quantity-reference.rst new file mode 100644 index 00000000..9d98503d --- /dev/null +++ b/xo-unit/docs/quantity-reference.rst @@ -0,0 +1,31 @@ +.. _quantity-reference: + +Quantity Reference +================== + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + |cBLU quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. toctree:: + :maxdepth: 2 + + quantity-class + quantity-factoryfunctions + quantity-unitvars + quantity-source-code diff --git a/xo-unit/docs/quantity-source-code.rst b/xo-unit/docs/quantity-source-code.rst new file mode 100644 index 00000000..bdc2a7fd --- /dev/null +++ b/xo-unit/docs/quantity-source-code.rst @@ -0,0 +1,11 @@ +.. _quantity-source-code: + +Quantity Source Code +==================== + +github: `quantity.hpp @github`_ + +.. _`quantity.hpp @github`: https://github.com/rconybea/xo-unit/blob/main/include/xo/unit/quantity.hpp + +.. literalinclude:: ../include/xo/unit/quantity.hpp + :language: c++ diff --git a/xo-unit/docs/quantity-unitvars.rst b/xo-unit/docs/quantity-unitvars.rst new file mode 100644 index 00000000..ee32e4da --- /dev/null +++ b/xo-unit/docs/quantity-unitvars.rst @@ -0,0 +1,98 @@ +.. _quantity-unitvars: + +Quantity Unit Variables +======================= + +Built-in unit quantities + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + |cBLU quantity | xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +.. code-block:: cpp + + #include + +The ``xo::qty::qty`` namespace contains unit quantities in each dimension. +Can use these to assemble unit quantities with compound dimensions + +.. code-block:: cpp + :linenos: + :emphasize-lines: 6 + + #include "xo/unit/quantity.hpp" + + namespace q = xo::qty::qty; + + auto q1 = (q::kilometers(150.0) / q::hours(0.5)); + constexpr auto u_mps = q:meter / q:second; + auto q2 = with_units_from(q1, u_mps); + + +Mass +---- +.. doxygenvariable:: xo::qty::qty::picogram +.. doxygenvariable:: xo::qty::qty::nanogram +.. doxygenvariable:: xo::qty::qty::microgram +.. doxygenvariable:: xo::qty::qty::milligram +.. doxygenvariable:: xo::qty::qty::gram +.. doxygenvariable:: xo::qty::qty::kilogram +.. doxygenvariable:: xo::qty::qty::tonne +.. doxygenvariable:: xo::qty::qty::kilotonne +.. doxygenvariable:: xo::qty::qty::megatonne +.. doxygenvariable:: xo::qty::qty::gigatonne + +Distance +-------- +.. doxygenvariable:: xo::qty::qty::picometer +.. doxygenvariable:: xo::qty::qty::nanometer +.. doxygenvariable:: xo::qty::qty::micrometer +.. doxygenvariable:: xo::qty::qty::millimeter +.. doxygenvariable:: xo::qty::qty::meter +.. doxygenvariable:: xo::qty::qty::kilometer +.. doxygenvariable:: xo::qty::qty::megameter +.. doxygenvariable:: xo::qty::qty::gigameter +.. doxygenvariable:: xo::qty::qty::lightsecond +.. doxygenvariable:: xo::qty::qty::astronomicalunit +.. doxygenvariable:: xo::qty::qty::inch +.. doxygenvariable:: xo::qty::qty::foot +.. doxygenvariable:: xo::qty::qty::yard +.. doxygenvariable:: xo::qty::qty::mile + +Time +---- +.. doxygenvariable:: xo::qty::qty::picosecond +.. doxygenvariable:: xo::qty::qty::nanosecond +.. doxygenvariable:: xo::qty::qty::microsecond +.. doxygenvariable:: xo::qty::qty::millisecond +.. doxygenvariable:: xo::qty::qty::second +.. doxygenvariable:: xo::qty::qty::minute +.. doxygenvariable:: xo::qty::qty::hour +.. doxygenvariable:: xo::qty::qty::day +.. doxygenvariable:: xo::qty::qty::week +.. doxygenvariable:: xo::qty::qty::month +.. doxygenvariable:: xo::qty::qty::year +.. doxygenvariable:: xo::qty::qty::year250 +.. doxygenvariable:: xo::qty::qty::year360 +.. doxygenvariable:: xo::qty::qty::year365 diff --git a/xo-unit/docs/scaled-unit-class.rst b/xo-unit/docs/scaled-unit-class.rst new file mode 100644 index 00000000..42725e00 --- /dev/null +++ b/xo-unit/docs/scaled-unit-class.rst @@ -0,0 +1,165 @@ +.. _scaled-unit-class: + +Scaled Unit +=========== + +A dimensionless multiple of a :doc:`natural_unit` + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+----------------+ + | quantity | xquantity | + +----------------+----------------+ + |cBLU scaled_unit | + +---------------------------------+ + | natural_unit | + +---------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+----------------+ + | basis_unit | + +---------------------------------+ + | dimension | + +---------------------------------+ + +Introduction +------------ + +.. code-block::cpp + + #include + +Extension of :doc:`natural_unit` to enable representing the intermediate +result of multiplication (or division) of natural units. + +- represents a (dimensionless) multiple of a cartesian product of basis units. +- constexpr implementation +- limited support for fractional dimensions such as time^-1/2 + +.. uml:: + :caption: scaled unit after (u::meter * u::foot / u::minute) + :scale: 99% + :align: center + + object area_per_time<> + area_per_time : outer_scale_factor = 3048/10000 + area_per_time : outer_scale_sq = 1.0 + area_per_time : natural_unit = m2_per_min + + object m2_per_min<> + m2_per_min : n_bpu = 2 + m2_per_min : bpu_v[] + + object m2<> + m2 : native_dim = dim::distance + m2 : scalefactor = 1/1 + m2 : power = 2/1 + + object min<> + min : native_dim = dim::time + min : scalefactor = 60/1 + min : power = -1/1 + + area_per_time o-- m2_per_min + m2_per_min o-- m2 + m2_per_min o-- min + +Scaled units with non-unity outer scalefactors arise as intermediate results +of quantity arithmetic + +Motivation +^^^^^^^^^^ + +Consider multiplying two units: + +.. code-block:: cpp + + using namespace xo::qty; + + constexpr auto u_prod = u::meter * u::kilometer; + +How should we represent the product? + +We don't want to mix units. Instead we consolidate on a common unit; +to do this we accumulate a product of conversion factors from such consolidation. + +For example: + +.. code-block:: cpp + :emphasize-lines: 3 + + static_assert(u_prod.n_bpu() == 1); + static_assert(u_prod[0].bu() == detail::bu::meter); + static_assert(u_prod[0].power() == power_ratio_type(2)); + static_assert(u_prod.outer_scale_factor_ == xo::ratio::ratio(1000)); + static_assert(u_prod.outer_scale_sq_ == 1.0); // used if fractional dimension + +Here we accumulate :code:`1000`, from converting kilometers to meters. + +Division works similarly. In this example dimension cancel, but we still have a non-unity conversion factor. + +.. code-block:: cpp + :emphasize-lines: 7 + + namespace u = xo::qty::u; + + constexpr auto u_div = u::meter / u::kilometer; + + // dimensionlesss result + static_assert(u_prod.n_bpu() == 0); + static_assert(u_prod.outer_scale_factor_ == xo::ratio::ratio(1,1000)); + static_assert(u_prod.outer_scale_sq_ == 1.0); + +When multiple dimensions needing conversion are involved, scalefactors accumulate: + +.. code-block:: cpp + :emphasize-lines: 8 + + namespace u = xo::qty::u; + + constexpr auto u2_prod = u::meter * u::hour * u::kilometer * u::minute; + + static_assert(u2_prod.n_bpu() == 2); + static_assert(u2_prod[0].bu() == detail::bu::meter); + static_assert(u2_prod[1].bu() == detail::bu::hour); + static_assert(u2_prod.outer_scale_factor_ == xo::ratio::ratio(50,3)); + static_assert(u2_prod.outer_scale_sq_ == 1.0); // used if fractional dimension + +Here the :code:`50/3` result comes from multiplying :code:`1000/1` (converting kilometers -> meters) +by :code:`1/60` (converting minutes -> hours) + + +Class +----- + +.. doxygenclass:: xo::qty::scaled_unit + +Member Variables +---------------- + +.. doxygengroup:: scaled-unit-instance-vars + +Type Traits +----------- + +.. doxygengroup:: scaled-unit-type-traits + +Access Methods +-------------- + +.. doxygengroup:: scaled-unit-access-methods + +General Methods +--------------- + +.. doxygengroup:: scaled-unit-general-methods + +Operators +--------- + +.. doxygengroup:: scaled-unit-operators diff --git a/xo-unit/docs/scaled-unit-constants.rst b/xo-unit/docs/scaled-unit-constants.rst new file mode 100644 index 00000000..f0b00b64 --- /dev/null +++ b/xo-unit/docs/scaled-unit-constants.rst @@ -0,0 +1,75 @@ +.. _scaled-unit-constants + +Scaled Unit Constants +===================== + +Built-in unit constants, for use with conversions + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + |cBLU scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +Introduction +------------ + +Built-in units. Application code may use these to trigger conversion + +.. code-block:: cpp + :emphasize-lines: 5 + + #include + + using namespace xo::qty; + + constexpr quantity q1 = q::miles(60) / q::hour; + +Note that it's often easiest to use :doc:`unit quantity constants`, +like :code:`q::hour` in the example above + +Dimensionless Constant +---------------------- + +.. doxygengroup:: scaled-unit-dimensionless + +Mass Units +---------- + +.. doxygengroup:: scaled-unit-mass + +Distance Units +-------------- + +.. doxygengroup:: scaled-unit-distance + +Time Units +---------- + +.. doxygengroup:: scaled-unit-time + +Volatility Units +---------------- + +.. doxygengroup:: scaled-unit-volatility + +Miscellaneous Units +------------------- + +.. doxygengroup:: scaled-unit-misc diff --git a/xo-unit/docs/scaled-unit-reference.rst b/xo-unit/docs/scaled-unit-reference.rst new file mode 100644 index 00000000..699db709 --- /dev/null +++ b/xo-unit/docs/scaled-unit-reference.rst @@ -0,0 +1,31 @@ +.. _scaled-unit-reference: + +Scaled Unit Reference +===================== + +A dimensionless multiple of a :doc:`natural_unit` + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity | xquantity | + +----------------+---------------+ + |cBLU scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. toctree:: + :maxdepth: 1 + + Scaled Unit Class + scaled-unit-constants diff --git a/xo-unit/docs/ubuntu-github-workflow.rst b/xo-unit/docs/ubuntu-github-workflow.rst new file mode 100644 index 00000000..012ee9a4 --- /dev/null +++ b/xo-unit/docs/ubuntu-github-workflow.rst @@ -0,0 +1,9 @@ +.. ubuntu-github-workflow: + +Ubuntu Github Workflow +====================== + +Instructions for build starting on a stock ubuntu platform (see `xo-unit/.github/workflows/ubuntu_main.yml`) + +.. literalinclude:: ../.github/workflows/ubuntu-main.yml + :language: yaml diff --git a/xo-unit/docs/xquantity-class.rst b/xo-unit/docs/xquantity-class.rst new file mode 100644 index 00000000..7d4de556 --- /dev/null +++ b/xo-unit/docs/xquantity-class.rst @@ -0,0 +1,96 @@ +.. _xquantity-class: + +Xquantity +========= + +Polymorphic dimensioned quantity with runtime unit checking/conversion + +Context +------- + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity |cBLU xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. code-block:: cpp + + #include + +.. uml:: + :scale: 99% + :align: center + + allowmixing + + object qty1<> + qty1 : scale = 1.23 + qty1 : unit = unit + + object unit<> + unit : is_natural() = true + + qty1 o-- unit + + +Class +----- + +Class with run-time unit representation. + +.. doxygenclass:: xo::qty::xquantity + +Member Variables +---------------- + +.. doxygengroup:: xquantity-instance-vars + +Constructors +------------ + +.. doxygengroup:: xquantity-ctors + +Access Methods +-------------- + +.. doxygengroup:: xquantity-access-methods + +Constants +--------- + +.. doxygengroup:: xquantity-constants + +Conversion Methods +------------------ + +.. doxygengroup:: xquantity-unit-conversion + +Arithmetic +---------- + +.. doxygengroup:: xquantity-operators + +Support methods for arithmetic operations + +.. doxygengroup:: xquantity-arithmetic-support + +Comparison +---------- + +Support methods for comparison operators + +.. doxygengroup:: xquantity-comparison-support diff --git a/xo-unit/docs/xquantity-reference.rst b/xo-unit/docs/xquantity-reference.rst new file mode 100644 index 00000000..2d34cee3 --- /dev/null +++ b/xo-unit/docs/xquantity-reference.rst @@ -0,0 +1,29 @@ +.. _xquantity-reference: + +Xquantity Reference +=================== + +.. ditaa:: + :--scale: 0.85 + + +----------------+---------------+ + | quantity |cBLU xquantity | + +----------------+---------------+ + | scaled_unit | + +--------------------------------+ + | natural_unit | + +--------------------------------+ + | bpu | + +----------------+ | + | bu_store | | + +----------------+---------------+ + | basis_unit | + +--------------------------------+ + | dimension | + +--------------------------------+ + +.. toctree:: + :maxdepth: 2 + + xquantity-class + xquantity-source-code diff --git a/xo-unit/docs/xquantity-source-code.rst b/xo-unit/docs/xquantity-source-code.rst new file mode 100644 index 00000000..5746b0e7 --- /dev/null +++ b/xo-unit/docs/xquantity-source-code.rst @@ -0,0 +1,11 @@ +.. _xquantity-source-code: + +Xquantity Source Code +===================== + +github: `xquantity.hpp @github`_ + +.. _`xquantity.hpp @github`: https://github.com/rconybea/xo-unit/blob/main/include/xo/unit/xquantity.hpp + +.. literalinclude:: ../include/xo/unit/xquantity.hpp + :language: c++ diff --git a/xo-unit/example/CMakeLists.txt b/xo-unit/example/CMakeLists.txt new file mode 100644 index 00000000..42c20ac6 --- /dev/null +++ b/xo-unit/example/CMakeLists.txt @@ -0,0 +1,10 @@ +add_subdirectory(ex1) +add_subdirectory(ex2) +add_subdirectory(ex3) +add_subdirectory(ex4) +add_subdirectory(ex5) +add_subdirectory(ex6) +add_subdirectory(ex7) +add_subdirectory(ex8) +add_subdirectory(ex_su) +add_subdirectory(ex_qty) diff --git a/xo-unit/example/ex1/CMakeLists.txt b/xo-unit/example/ex1/CMakeLists.txt new file mode 100644 index 00000000..fb1b6879 --- /dev/null +++ b/xo-unit/example/ex1/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex1/CMakeLists.txt + +set(SELF_EXE xo_unit_ex1) +set(SELF_SRCS ex1.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex1/ex1.cpp b/xo-unit/example/ex1/ex1.cpp new file mode 100644 index 00000000..f1dfe275 --- /dev/null +++ b/xo-unit/example/ex1/ex1.cpp @@ -0,0 +1,53 @@ +/** @file ex1.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + namespace q = xo::qty::qty; + namespace u = xo::qty::u; + using xo::qty::quantity; + using xo::flatstring; + using namespace std; + + constexpr auto t = q::minutes(2); + constexpr auto d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + cerr << "t: " << t << ", d: " << d + << ", t^2: " << t2 + << ", d.t^-2: " << a + << endl; + + static_assert(std::same_as); + static_assert(sizeof(t) == sizeof(int)); + static_assert(t.scale() == 2); + static_assert(t.abbrev() == flatstring("min")); + + static_assert(std::same_as); + static_assert(sizeof(d) == sizeof(double)); + static_assert(d.scale() == 2.5); + static_assert(d.abbrev() == flatstring("km")); + + static_assert(std::same_as); + static_assert(sizeof(t2) == sizeof(int)); + static_assert(t2.scale() == 4); + static_assert(t2.abbrev() == flatstring("min^2")); + + static_assert(std::same_as); + static_assert(sizeof(a) == sizeof(double)); + static_assert(a.scale() == 0.625); + static_assert(a.abbrev() == flatstring("km.min^-2")); + + constexpr auto a2 = a.rescale_ext<(u::meter / (u::second * u::second))>(); + + cerr << "d.t^-2: " << a2 << endl; + + static_assert(a2.abbrev() == flatstring("m.s^-2")); +} + +/** end ex1.cpp **/ diff --git a/xo-unit/example/ex2/CMakeLists.txt b/xo-unit/example/ex2/CMakeLists.txt new file mode 100644 index 00000000..82406fe6 --- /dev/null +++ b/xo-unit/example/ex2/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex2/CMakeLists.txt + +set(SELF_EXE xo_unit_ex2) +set(SELF_SRCS ex2.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex2/ex2.cpp b/xo-unit/example/ex2/ex2.cpp new file mode 100644 index 00000000..94fa9054 --- /dev/null +++ b/xo-unit/example/ex2/ex2.cpp @@ -0,0 +1,49 @@ +/** @file ex2.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + namespace q = xo::qty::qty; + namespace u = xo::qty::u; + using xo::qty::with_units_from; + using xo::qty::with_units; + using xo::qty::quantity; + using xo::flatstring; + using namespace std; + + constexpr auto t = q::minutes(2); + constexpr auto d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + cerr << "t: " << t << ", d: " << d + << ", t^2: " << t2 + << ", d.t^-2: " << a + << endl; + + constexpr auto a2 = a.rescale_ext(); + + static_assert(a2.abbrev() == flatstring("m.s^-2")); + + cerr << "a2: " << a2 << endl; + + constexpr auto a3 = with_units(a); + + static_assert(a3.abbrev() == flatstring("m.s^-2")); + + cerr << "a3: " << a3 << endl; + + constexpr auto au = q::meter / (q::second * q::second); + constexpr auto a4 = with_units_from(a, au); + + static_assert(a4.abbrev() == flatstring("m.s^-2")); + + cerr << "a4: " << a4 << endl; +} + + +/** end ex2.cpp **/ diff --git a/xo-unit/example/ex3/CMakeLists.txt b/xo-unit/example/ex3/CMakeLists.txt new file mode 100644 index 00000000..06410943 --- /dev/null +++ b/xo-unit/example/ex3/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex3/CMakeLists.txt + +set(SELF_EXE xo_unit_ex3) +set(SELF_SRCS ex3.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex3/ex3.cpp b/xo-unit/example/ex3/ex3.cpp new file mode 100644 index 00000000..d63bb71d --- /dev/null +++ b/xo-unit/example/ex3/ex3.cpp @@ -0,0 +1,25 @@ +/** @file ex3.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + namespace q = xo::qty::qty; + namespace u = xo::qty::u; + using xo::qty::quantity; + using namespace std; + + constexpr quantity t = q::minutes(2); + constexpr quantity d = q::kilometers(2.5); + + constexpr auto t2 = t*t; + constexpr auto a = d / (t*t); + + cerr << "t: " << t << ", d: " << d + << ", d.t^-2: " << a + << endl; +} + +/** end ex3.cpp **/ diff --git a/xo-unit/example/ex4/CMakeLists.txt b/xo-unit/example/ex4/CMakeLists.txt new file mode 100644 index 00000000..cb1ce76b --- /dev/null +++ b/xo-unit/example/ex4/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex4/CMakeLists.txt + +set(SELF_EXE xo_unit_ex4) +set(SELF_SRCS ex4.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex4/ex4.cpp b/xo-unit/example/ex4/ex4.cpp new file mode 100644 index 00000000..6691bd7d --- /dev/null +++ b/xo-unit/example/ex4/ex4.cpp @@ -0,0 +1,30 @@ +/** @file ex4.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + namespace q = xo::qty::qty; + + auto t1 = q::milliseconds(1); + auto t2 = q::minutes(1); + + auto r1 = t1 / with_repr(t2); + + static_assert(r1.is_dimensionless()); + static_assert(!t2.is_dimensionless()); + + static_assert(std::same_as(r1)), double>); + + /* r1_value: assignment compiles, since r1 dimensionless */ + double r1_value = r1; + + /* r2_value: assignment won't compile, 'cannot convert' error */ + //double r2_value = t2; + + std::cerr << "t1: " << t1 << ", t2: " << t2 << ", t1/t2: " << r1 << std::endl; +} + +/** end ex4.cpp */ diff --git a/xo-unit/example/ex5/CMakeLists.txt b/xo-unit/example/ex5/CMakeLists.txt new file mode 100644 index 00000000..3d59c0ff --- /dev/null +++ b/xo-unit/example/ex5/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex5/CMakeLists.txt + +set(SELF_EXE xo_unit_ex5) +set(SELF_SRCS ex5.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex5/ex5.cpp b/xo-unit/example/ex5/ex5.cpp new file mode 100644 index 00000000..b67e8e84 --- /dev/null +++ b/xo-unit/example/ex5/ex5.cpp @@ -0,0 +1,24 @@ +/** @file ex5.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + //namespace u = xo::unit::units; + namespace q = xo::qty::qty; + using namespace std; + + /* 20% volatility over 250 days (approx number of trading days in one year) */ + auto q1 = q::volatility_250d(0.2); + /* 10% volatility over 30 days */ + auto q2 = q::volatility_30d(0.1); + + auto sum = q1 + q2; + auto prod = q1 * q2; + + cerr << "q1: " << q1 << ", q2: " << q2 << ", q1+q2: " << sum << ", q1*q2: " << prod << endl; +} + +/** end ex5.cpp */ diff --git a/xo-unit/example/ex6/CMakeLists.txt b/xo-unit/example/ex6/CMakeLists.txt new file mode 100644 index 00000000..bbb62dd1 --- /dev/null +++ b/xo-unit/example/ex6/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex6/CMakeLists.txt + +set(SELF_EXE xo_unit_ex6) +set(SELF_SRCS ex6.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex6/ex6.cpp b/xo-unit/example/ex6/ex6.cpp new file mode 100644 index 00000000..f7ff86a4 --- /dev/null +++ b/xo-unit/example/ex6/ex6.cpp @@ -0,0 +1,36 @@ +/** @file ex6.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + using xo::qty::quantity; + namespace q = xo::qty::qty; + using xo::flatstring; + + /* 20% volatility over 360 days */ + auto q1 = q::volatility_360d(0.2); + /* 10% volatility over 30 days */ + auto q2 = q::volatility_30d(0.1); + + /* 10% volatility per 30 days + * ~ (10% * sqrt(360/30)) volatility over 360 days + * ~ (10% * 3.4641) + * ~ 0.34641yr360^(-1/2) + */ + + auto sum = q1 + q2; + auto prod = q1 * q2; + + static_assert(sum.abbrev() == flatstring("yr360^(-1/2)")); + static_assert(prod.abbrev() == flatstring("yr360^-1")); + + std::cerr << "q1: " << q1 << std::endl; + std::cerr << "q2: " << q2 << std::endl; + std::cerr << "q1+q2: " << sum << std::endl; + std::cerr << "q1*q2: " << prod << std::endl; +} + +/** end ex6.cpp */ diff --git a/xo-unit/example/ex7/CMakeLists.txt b/xo-unit/example/ex7/CMakeLists.txt new file mode 100644 index 00000000..a13bc9f7 --- /dev/null +++ b/xo-unit/example/ex7/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex7/CMakeLists.txt + +set(SELF_EXE xo_unit_ex7) +set(SELF_SRCS ex7.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex7/ex7.cpp b/xo-unit/example/ex7/ex7.cpp new file mode 100644 index 00000000..7b617176 --- /dev/null +++ b/xo-unit/example/ex7/ex7.cpp @@ -0,0 +1,31 @@ +/** @file ex7.cpp **/ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include + +int +main () { + using namespace xo::qty; + namespace u = xo::qty::u; + namespace q = xo::qty::qty; + using namespace std; + + quantity qty1 = 7.55 * q::kilometer / q::minute / q::minute; + quantity qty2 = q::nanograms(123); + quantity qty3 = qty2 * qty1; + + cerr << "qty1: " << qty1 << endl; + cerr << "qty2: " << qty2 << endl; + cerr << "qty3: " << qty3 << endl; + + /* rescale to not-so-absurd units */ + + /* kg.m.s^-2 */ + quantity res = qty3.rescale_ext(); + + /* 2.57958e-10kg.m.s^-2 */ + cerr << "res: " << res << endl; +} + +/** end ex7.cpp */ diff --git a/xo-unit/example/ex8/CMakeLists.txt b/xo-unit/example/ex8/CMakeLists.txt new file mode 100644 index 00000000..42681a72 --- /dev/null +++ b/xo-unit/example/ex8/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex8/CMakeLists.txt + +set(SELF_EXE xo_unit_ex8) +set(SELF_SRCS ex8.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex8/ex8.cpp b/xo-unit/example/ex8/ex8.cpp new file mode 100644 index 00000000..c8aaf4bc --- /dev/null +++ b/xo-unit/example/ex8/ex8.cpp @@ -0,0 +1,37 @@ +/** @file ex8.cpp **/ + +#include "xo/unit/xquantity.hpp" +#include "xo/unit/xquantity_iostream.hpp" +#include + +int +main () { + using namespace xo::qty; + namespace u = xo::qty::u; + using namespace std; + + constexpr xquantity qty1(7, u::foot); + constexpr xquantity qty2(6.0, u::inch); + + // constexpr not supported for xquantity addition + xquantity qty3 = qty1 + qty2; + + cerr << "qty1: " << qty1 << endl; + cerr << "qty2: " << qty2 << endl; + cerr << "qty3: " << qty3 << endl; + + /* rescale to mm */ + xquantity res = qty3.rescale_ext(xo::qty::u::millimeter); + + /* 2286mm */ + cerr << "res: " << res << endl; + + /* 12 */ + xquantity qty4 = qty1 / qty2; + + auto res2 = qty4 + 4; + + cerr << "res2: " << res << endl; +} + +/** end ex8.cpp */ diff --git a/xo-unit/example/ex_qty/CMakeLists.txt b/xo-unit/example/ex_qty/CMakeLists.txt new file mode 100644 index 00000000..5da961a4 --- /dev/null +++ b/xo-unit/example/ex_qty/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/ex_qty/CMakeLists.txt + +set(SELF_EXE xo_unit_ex_qty) +set(SELF_SRCS ex_qty.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex_qty/ex_qty.cpp b/xo-unit/example/ex_qty/ex_qty.cpp new file mode 100644 index 00000000..b9c9a087 --- /dev/null +++ b/xo-unit/example/ex_qty/ex_qty.cpp @@ -0,0 +1,18 @@ +/* @file ex_qty.cpp */ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" + +using namespace std; + +int +main() { + using namespace xo::qty; + namespace u = xo::qty::u; + + static_assert(u::meter.n_bpu() == 1); + + //constexpr auto q = qty::meters(2) + u::meter; +} + +/* end ex_qty.cpp */ diff --git a/xo-unit/example/ex_su/CMakeLists.txt b/xo-unit/example/ex_su/CMakeLists.txt new file mode 100644 index 00000000..bad7a1ff --- /dev/null +++ b/xo-unit/example/ex_su/CMakeLists.txt @@ -0,0 +1,12 @@ +# xo-unit/example/su/CMakeLists.txt + +set(SELF_EXE xo_unit_ex_su) +set(SELF_SRCS ex_su.cpp) + +if (XO_ENABLE_EXAMPLES) + xo_add_executable(${SELF_EXE} ${SELF_SRCS}) + xo_self_headeronly_dependency(${SELF_EXE} xo_unit) + xo_dependency(${SELF_EXE} xo_flatstring) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/example/ex_su/ex_su.cpp b/xo-unit/example/ex_su/ex_su.cpp new file mode 100644 index 00000000..67a67903 --- /dev/null +++ b/xo-unit/example/ex_su/ex_su.cpp @@ -0,0 +1,38 @@ +/** @file ex_su.cpp **/ + +#include "xo/unit/scaled_unit.hpp" +#include "xo/unit/scaled_unit_iostream.hpp" +#include + +using namespace std; + +int +main() { + using namespace xo::qty; + + constexpr auto u_prod = u::meter * u::kilometer; + + static_assert(u_prod[0].bu() == detail::bu::meter); + static_assert(u_prod[0].power() == power_ratio_type(2)); + static_assert(u_prod.outer_scale_factor_ == xo::ratio::ratio(1000)); + static_assert(u_prod.outer_scale_sq_ == 1.0); // used if fractional dimension + + constexpr auto u_div = u::meter / u::kilometer; + + static_assert(u_div.n_bpu() == 0); + static_assert(u_div.outer_scale_factor_ == xo::ratio::ratio(1,1000)); + static_assert(u_prod.outer_scale_sq_ == 1.0); // used if fractional dimension + + constexpr auto u2_prod = u::meter * u::hour * u::kilometer * u::minute; + + static_assert(u2_prod.n_bpu() == 2); + static_assert(u2_prod[0].bu() == detail::bu::meter); + static_assert(u2_prod[1].bu() == detail::bu::hour); + // *1000 from converting kilometers -> meters + // /60 from converting minutes -> hours + // 1000/60 = 50/3 in lowest terms + static_assert(u2_prod.outer_scale_factor_ == xo::ratio::ratio(50,3)); // used if fractional dimension + static_assert(u2_prod.outer_scale_sq_ == 1.0); // used if fractional dimension +} + +/** end ex_su.cpp **/ diff --git a/xo-unit/flake.nix b/xo-unit/flake.nix new file mode 100644 index 00000000..88751d06 --- /dev/null +++ b/xo-unit/flake.nix @@ -0,0 +1,299 @@ +{ + description = "xo-unit: c++ compile-time dimension checking and unit conversion"; + + # Adapted from xo-nix2/flake.nix + + # MANIFESTO + # No build instructions in flake.nix + # - Following Jade Lovelace's advice + # - Build instructions are in pkgs/*.nix + # - Each pkgs/*.nix is intended to work 'like a .nix file in nixpkgs' + # I'm being lazy about source hashes, since flake.nix supplies them. + # + # Motivation (per JL) versus doing everything in flake.nix: + # - nixpkgs-ready + # - parameterized + # - overridable + # - still works if cross-compiling + # + # Instead: using flake.nix as entry point: + # - pin nixpkgs to a specific revision, for reproducibility + # - pin our candidate packages (pkgs/*.nix), for the same reason. + + # to determine specific hash for nixpkgs: + # 1. $ cd ~/proj/nixpkgs + # 2. $ git checkout release-23.05 + # 3. $ git fetch + # 4. $ git pull + # 5. $ git log -1 + # take this hash, then substitue for ${hash} in: + # inputs.nixpkgs.url = "https://github.com/NixOS/nixpkgs/archive/${hash}.tar.gz"; + # below + #inputs.nixpkgs.url = "https://github.com/NixOS/nixpkgs/archive/9a333eaa80901efe01df07eade2c16d183761fa3.tar.gz"; + + # as sbove but instead of {release-23.05} use {release-23.11} + # gcc -> 12.3.0 + # python -> 3.11.6 + # + inputs.nixpkgs.url = "https://github.com/NixOS/nixpkgs/archive/217b3e910660fbf603b0995a6d2c3992aef4cc37.tar.gz"; # asof 10mar2024 + #inputs.nixpkgs.url = "https://github.com/NixOS/nixpkgs/archive/4dd376f7943c64b522224a548d9cab5627b4d9d6.tar.gz"; + + # inputs.nixpkgs.url + # = "https://github.com/NixOS/nixpkgs/archive/fac3684647cc9d6dfb2a39f3f4b7cf5fc89c96b6.tar.gz"; # asof 8feb2024 + # fac3684647.. asof 17oct2023 + # instead of + # inputs.nixpkgs.url = "github:nixos/nixpkgs/23.05"; + + inputs.flake-utils.url = "github:numtide/flake-utils"; + + # To add a new package, visit placeholder-A .. placeholder-E + + inputs.xo-cmake-path = { type = "github"; owner = "Rconybea"; repo = "xo-cmake"; flake = false; }; + inputs.xo-indentlog-path = { type = "github"; owner = "Rconybea"; repo = "indentlog"; flake = false; }; + inputs.xo-flatstring-path = { type = "github"; owner = "Rconybea"; repo = "xo-flatstring"; flake = false; }; + inputs.xo-refcnt-path = { type = "github"; owner = "Rconybea"; repo = "refcnt"; flake = false; }; + inputs.xo-subsys-path = { type = "github"; owner = "Rconybea"; repo = "subsys"; flake = false; }; + inputs.xo-randomgen-path = { type = "github"; owner = "Rconybea"; repo = "randomgen"; flake = false; }; + inputs.xo-ratio-path = { type = "github"; owner = "Rconybea"; repo = "xo-ratio"; flake = false; }; + #inputs.xo-pyutil-path = { type = "github"; owner = "Rconybea"; repo = "xo-pyutil"; flake = false; }; + inputs.xo-reflect-path = { type = "github"; owner = "Rconybea"; repo = "reflect"; flake = false; }; + #inputs.xo-pyreflect-path = { type = "github"; owner = "Rconybea"; repo = "xo-pyreflect"; flake = false; }; + + # placeholder-A + + outputs + = { self, + nixpkgs, + flake-utils, + xo-cmake-path, + xo-indentlog-path, + xo-flatstring-path, + xo-refcnt-path, + xo-subsys-path, + xo-randomgen-path, + xo-ratio-path, + xo-reflect-path, + # placeholder-B + } : + # out :: system -> {packages, devShells} + let + out + = system : + let + pkgs = nixpkgs.legacyPackages.${system}; + + # could try using + # appliedOverlay = (pkgs.extend self.overlays.default) + # but it doesn't seem to work the way I expect, + # For example, wants to pickup 2.7.11 python for xo-pyutil ! + # + appliedOverlay = self.overlays.default pkgs pkgs; + + in + { + #xo-cmake-dir = "${self.packages.${system}.xo-cmake}/share/cmake"; + + # reminder: + # 'packages' comprises the output of this flake; + # each defn invokes a build + # ./pkgs/$example.nix + # using + # cmake-examples-$example-path + # above for source code + + packages.xo-cmake = appliedOverlay.xo-cmake; + packages.xo-indentlog = appliedOverlay.xo-indentlog; + packages.xo-flatstring = appliedOverlay.xo-flatstring; + packages.xo-refcnt = appliedOverlay.xo-refcnt; + packages.xo-subsys = appliedOverlay.xo-subsys; + packages.xo-randomgen = appliedOverlay.xo-randomgen; + packages.xo-ratio = appliedOverlay.xo-ratio; + packages.xo-reflect = appliedOverlay.xo-reflect; + packages.xo-unit = appliedOverlay.xo-unit; + # placeholder-C + + packages.xo-userenv = appliedOverlay.xo-userenv; + + devShells = appliedOverlay.devShells; + }; + in + flake-utils.lib.eachDefaultSystem + out + // + { + # introduce overlay to extend nixpkgs with our local packages, + # (which ofc are not present in nixpkgs, though same form would work if they were present) + # + overlays.default = final: prev: + ( + let + # can use + # $ nix-env -qaP | grep \.boost # show known boost versions + # $ nix-env -qaP | grep \.python.*Packages # show known python versions + + stdenv = prev.stdenv; + + boost = prev.boost182; + python = prev.python311Full; + pythonPackages = prev.python311Packages; + #doxygen = prev.doxygen; + + pybind11 = pythonPackages.pybind11; + #breathe = pythonPackages.breathe; + #sphinx = pythonPackages.sphinx; + #sphinx-rtd-theme = pythonPackages.sphinx-rtd-theme; + + #extras1 = { boost = boost; }; + #extras2 = { boost = boost; python3Packages = python3Packages; pybind11 = pybind11; }; + #extras3 = { boost = boost; python3Packages = python3Packages; pybind11 = pybind11; doxygen = doxygen; }; + #extras4 = extras3 // { breathe = breathe; }; + + xo-cmake = + (prev.callPackage ./pkgs/xo-cmake.nix {}).overrideAttrs + (old: { src = xo-cmake-path; }); + + xo-indentlog = + (prev.callPackage ./pkgs/xo-indentlog.nix { xo-cmake = xo-cmake; }).overrideAttrs + (old: { src = xo-indentlog-path; }); + + xo-flatstring = + (prev.callPackage ./pkgs/xo-flatstring.nix { xo-cmake = xo-cmake; + xo-indentlog = xo-indentlog; + python = python; + pythonPackages = pythonPackages; }).overrideAttrs + (old: { src = xo-flatstring-path; }); + + xo-subsys = + (prev.callPackage ./pkgs/xo-subsys.nix { xo-cmake = xo-cmake; }).overrideAttrs + (old: { src = xo-subsys-path; }); + + xo-randomgen = + (prev.callPackage ./pkgs/xo-randomgen.nix { xo-cmake = xo-cmake; + xo-indentlog = xo-indentlog; + }).overrideAttrs + (old: { src = xo-randomgen-path; }); + + xo-ratio = + (prev.callPackage ./pkgs/xo-ratio.nix { xo-cmake = xo-cmake; + xo-flatstring = xo-flatstring; + xo-randomgen = xo-randomgen; + python = python; + pythonPackages = pythonPackages; + }).overrideAttrs + (old: { src = xo-ratio-path; }); + + xo-refcnt = + (prev.callPackage ./pkgs/xo-refcnt.nix { xo-cmake = xo-cmake; + xo-indentlog = xo-indentlog; }).overrideAttrs + (old: { src = xo-refcnt-path; }); + + xo-reflect = + (prev.callPackage ./pkgs/xo-reflect.nix { xo-cmake = xo-cmake; + xo-subsys = xo-subsys; + xo-refcnt = xo-refcnt; }).overrideAttrs + (old: { src = xo-reflect-path; }); + + xo-unit = + (prev.callPackage ./pkgs/xo-unit.nix { xo-cmake = xo-cmake; + xo-flatstring = xo-flatstring; + xo-randomgen = xo-randomgen; + xo-ratio = xo-ratio; + python = python; + pythonPackages = pythonPackages; + }).overrideAttrs + (old: { src = ./.; }); + + # placeholder-D + + # user environment with all xo libraries present + xo-userenv = + (prev.callPackage ./pkgs/xo-userenv.nix { xo-cmake = xo-cmake; + xo-indentlog = xo-indentlog; + xo-flatstring = xo-flatstring; + xo-subsys = xo-subsys; + xo-refcnt = xo-refcnt; + xo-randomgen = xo-randomgen; + xo-ratio = xo-ratio; + xo-reflect = xo-reflect; + xo-unit = xo-unit; + }).overrideAttrs(old: {}); + + + in + # attrs in this set provide derivations with all overlay changes applied. + # + # REMINDER: need expression like + # packages.xo-foo = appliedOverlay.xo-foo; + # above to export + { + xo-cmake = xo-cmake; + xo-indentlog = xo-indentlog; + xo-flatstring = xo-flatstring; + xo-subsys = xo-subsys; + xo-refcnt = xo-refcnt; + xo-randomgen = xo-randomgen; + xo-ratio = xo-ratio; + xo-reflect = xo-reflect; + xo-unit = xo-unit; + # placeholder-E + + xo-userenv = xo-userenv; + + devShells = { + default = prev.mkShell.override + # but may need prev.clang16Stdenv instead of prev.stdenv here on macos + { stdenv = prev.stdenv; } + + { packages + = [ python + pybind11 + pythonPackages.coverage + pythonPackages.sphinx + pythonPackages.sphinx-rtd-theme + pythonPackages.breathe + # pythonPackages.pyarrow + boost # really for filemerge + + prev.llvmPackages_16.clang-unwrapped + + prev.anki + prev.mesa + prev.egl-wayland + + prev.emacs29 + prev.notmuch + prev.emacsPackages.notmuch + prev.inconsolata-lgc + + prev.doxygen + prev.graphviz + + prev.ditaa + prev.semgrep + prev.ripgrep + prev.git + prev.openssh + prev.cmake + prev.gdb + prev.which + prev.man + prev.man-pages + prev.less + prev.tree + prev.nix-tree + prev.lcov + + prev.arrow-cpp + prev.libwebsockets + prev.jsoncpp + prev.eigen + prev.catch2 + prev.pkg-config + prev.zlib + ]; + }; + }; + }); + }; + +} diff --git a/xo-unit/include/xo/unit/basis_unit.hpp b/xo-unit/include/xo/unit/basis_unit.hpp new file mode 100644 index 00000000..d856f8fc --- /dev/null +++ b/xo-unit/include/xo/unit/basis_unit.hpp @@ -0,0 +1,240 @@ +/** @file basis_unit.hpp **/ + +#pragma once + +#include "native_unit.hpp" +#include "dimension.hpp" +//#include "basis_unit_abbrev.hpp" +#include "xo/ratio/ratio.hpp" + +namespace xo { + namespace qty { + using bu_abbrev_type = flatstring<16>; + using scalefactor_ratio_type = xo::ratio::ratio; + using scalefactor2x_ratio_type = xo::ratio::ratio<__int128>; + + /** @class basis_unit + * @brief A dimensionless multiple of a single natively-specified basis dimension + * + * For example "3600 minutes" or "1e-6 grams". + * + * Members are public so that a @c basis_unit instance qualifies as a 'structural type', + * and therefore may be used as a non-type template parameter. + **/ + struct basis_unit { + public: + /** @defgroup basis-unit-constructors basis_unit constructors **/ + ///@{ + /** sentinel basis unit: invalid dimension and zero scalefactor **/ + constexpr basis_unit() = default; + /** basis unit representing multiple @p scalefactor of native dimension @p **/ + constexpr basis_unit(dimension native_dim, + const scalefactor_ratio_type & scalefactor) + : native_dim_{native_dim}, + scalefactor_{scalefactor} + {} + ///@} + + /** @defgroup basis-unit-access-methods basis_unit access methods **/ + ///@{ + /** get @c native_dim member **/ + constexpr dimension native_dim() const { return native_dim_; } + /** get @c scalefactor member **/ + constexpr const scalefactor_ratio_type & scalefactor() const { return scalefactor_; } + ///@} + + public: /* public so instance can be a non-type template parameter (a structural type) */ + /** @defgroup basis-unit-instance-vars basis_unit instance variables **/ + ///@{ + /** @brief identifies a native unit, e.g. time **/ + dimension native_dim_ = dimension::invalid; + /** @brief this unit defined as multiple scalefactor times native unit **/ + scalefactor_ratio_type scalefactor_ = {}; + ///@} + }; + + /** @defgroup basis-unit-comparison-support basis_unit comparisons **/ + ///@{ + /** @c true iff basis units are equal; + * both native dimension and scalefactor must be equal + **/ + inline constexpr bool + operator==(const basis_unit & x, const basis_unit & y) + { + return ((x.native_dim_ == y.native_dim_) + && (x.scalefactor_ == y.scalefactor_)); + } + + /** @c true iff bass units are not equal **/ + inline constexpr bool + operator!=(const basis_unit & x, const basis_unit & y) + { + return ((x.native_dim_ != y.native_dim_) + || (x.scalefactor_ != y.scalefactor_)); + } + ///@} + + namespace detail { + /** @brief namespace for basis-unit constants and helpers **/ + namespace bu { + // ----- mass ----- + + constexpr basis_unit mass_unit(std::int64_t num, std::int64_t den) { + return basis_unit(dimension::mass, scalefactor_ratio_type(num, den)); + } + + /** @defgroup basis-unit-mass-units basis_unit mass units **/ + ///@{ + /** basis unit of 10^-12 grams **/ + constexpr basis_unit picogram = mass_unit( 1, 1000000000000); + /** basis unit of 10^-9 grams **/ + constexpr basis_unit nanogram = mass_unit( 1, 1000000000); + /** basis unit of 10^-6 grams **/ + constexpr basis_unit microgram = mass_unit( 1, 1000000); + /** basis unit of 10^-3 grams **/ + constexpr basis_unit milligram = mass_unit( 1, 1000); + /** basis unit of 1 gram **/ + constexpr basis_unit gram = mass_unit( 1, 1); + /** basis unit of 10^3 grams **/ + constexpr basis_unit kilogram = mass_unit( 1000, 1); + /** basis unit of 10^6 grams = 10^3 kilograms **/ + constexpr basis_unit tonne = mass_unit( 1000000, 1); + /** basis unit of 10^9 grams = 10^6 kilograms = 10^3 tonnes **/ + constexpr basis_unit kilotonne = mass_unit( 1000000000, 1); + /** basis unit of 10^12 grams = 10^9 kilograms = 10^6 tonnes **/ + constexpr basis_unit megatonne = mass_unit( 1000000000000, 1); + /** basis unit of 10^15 grams = 10^12 kilograms = 10^9 tonnes **/ + constexpr basis_unit gigatonne = mass_unit(1000000000000000, 1); + ///@} + + // ----- distance ----- + + constexpr basis_unit distance_unit(std::int64_t num, std::int64_t den) { + return basis_unit(dimension::distance, scalefactor_ratio_type(num, den)); + } + + /** @defgroup basis-unit-distance-units basis_unit distance units **/ + ///@{ + /* US spelling */ + /** basis unit of 10^-12 meters **/ + constexpr basis_unit picometer = distance_unit( 1, 1000000000000); + /** basis unit of 10^-9 meters **/ + constexpr basis_unit nanometer = distance_unit( 1, 1000000000); + /** basis unit of 10^-6 meters **/ + constexpr basis_unit micrometer = distance_unit( 1, 1000000); + /** basis unit of 10^-3 meters **/ + constexpr basis_unit millimeter = distance_unit( 1, 1000); + /** basis unit of 1 meter **/ + constexpr basis_unit meter = distance_unit( 1, 1); + /** basis unit of 10^3 meters **/ + constexpr basis_unit kilometer = distance_unit( 1000, 1); + /** basis unit of 10^6 meters (for form's sake -- not commonly used) **/ + constexpr basis_unit megameter = distance_unit( 1000000, 1); + /** basis unit of 10^9 meters (for form's sake -- not commonly used) **/ + constexpr basis_unit gigameter = distance_unit( 1000000000, 1); + + /** basis unit of 1 light-second = distance light travels in a vacuum in 1 second **/ + constexpr basis_unit lightsecond = distance_unit( 299792458, 1); + /** basis unit of 1 astronomical unit, representing approximate radius of earth orbit. **/ + constexpr basis_unit astronomicalunit = distance_unit( 149597870700, 1); + + /* Int'l spelling */ + /** international spelling for picometer **/ + constexpr basis_unit picometre = picometer; + /** international spelling for nanometer **/ + constexpr basis_unit nanometre = nanometer; + /** international spelling for micrometer **/ + constexpr basis_unit micrometre = micrometer; + /** international spelling for millimeter **/ + constexpr basis_unit millimetre = millimeter; + /** international spelling for meter **/ + constexpr basis_unit metre = meter; + /** international spelling for kilometer **/ + constexpr basis_unit kilometre = kilometer; + /** international spelling for megameter **/ + constexpr basis_unit megametre = megameter; + /** international spelling for gigameter **/ + constexpr basis_unit gigametre = gigameter; + + /** @brief basis-unit representing 1 inch; defined as exactly 1/12 feet **/ + constexpr basis_unit inch = distance_unit( 3048, 120000); + /** @brief basis-unit representing 1 foot; defined as exactly 0.3048 meters **/ + constexpr basis_unit foot = distance_unit( 3048, 10000); + /** @brief basis-unit representing 1 yard; defined as exactly 3 feet **/ + constexpr basis_unit yard = distance_unit( 3*3048, 10000); + /** @brief basis-unit representing 1 mile; defined as exactly 1760 yards = 5280 feet **/ + constexpr basis_unit mile = distance_unit( 5280*3048, 10000); + ///@} + + // ----- time ----- + + constexpr basis_unit time_unit(std::int64_t num, std::int64_t den) { + return basis_unit(dimension::time, scalefactor_ratio_type(num, den)); + } + + /** @defgroup basis-unit-time-units basis_unit time units **/ + ///@{ + /** basis unit of 10^-12 seconds **/ + constexpr basis_unit picosecond = time_unit( 1, 1000000000000); + /** basis unit of 10^-9 seconds **/ + constexpr basis_unit nanosecond = time_unit( 1, 1000000000); + /** basis unit of 10^-6 seconds **/ + constexpr basis_unit microsecond = time_unit( 1, 1000000); + /** basis unit of 10^-3 seconds **/ + constexpr basis_unit millisecond = time_unit( 1, 1000); + /** basis unit of 1 second **/ + constexpr basis_unit second = time_unit( 1, 1); + /** basis unit of 1 minute = 60 seconds **/ + constexpr basis_unit minute = time_unit( 60, 1); + /** basis unit of 1 hour = 3600 seconds **/ + constexpr basis_unit hour = time_unit( 3600, 1); + /** basis unit of 1 day = exactly 24 hours **/ + constexpr basis_unit day = time_unit( 24*3600, 1); + /** basis unit of 1 week = exactly 7 days **/ + constexpr basis_unit week = time_unit( 7*24*3600, 1); + /** basis unit of 1 month = exactly 30 days **/ + constexpr basis_unit month = time_unit( 30*24*3600, 1); + /** basis unit of 1 year, defined as 365.25 days **/ + constexpr basis_unit year = time_unit( (365*24+6)*3600, 1); + + /* alt conventions used in finance */ + /** basis unit of 1 year365 = exactly 365 days **/ + constexpr basis_unit year365 = time_unit( 365*24*3600, 1); + /** basis unit of 1 year360 = exactly 360 days **/ + constexpr basis_unit year360 = time_unit( 360*24*3600, 1); + /** basis unit of 1 year250 = exactly 250 days. + * Approximate number of business days in one year + **/ + constexpr basis_unit year250 = time_unit( 250*24*3600, 1); + + //constexpr basis_unit century = time_unit( 100L*(365*24+6)*3600, 1); + //constexpr basis_unit millenium = time_unit(1000L*(365*24+6)*3600, 1); + ///@} + + // ----- currency ----- + + /** @defgroup basis-unit-misc-units basis_unit miscellaneous units**/ + ///@{ + + constexpr basis_unit currency_unit(std::int64_t num, std::int64_t den) { + return basis_unit(dimension::currency, scalefactor_ratio_type(num, den)); + } + + /** pseudounit -- placeholder for any actual currency amount **/ + constexpr basis_unit currency = currency_unit(1, 1); + + // ----- price ----- + + constexpr basis_unit price_unit(std::int64_t num, std::int64_t den) { + return basis_unit(dimension::price, scalefactor_ratio_type(num, den)); + } + + /** psuedounit -- context-dependent interpretation for a screen price **/ + constexpr basis_unit price = price_unit(1, 1); + ///@} + } /*namespace bu*/ + } /*namespace detail*/ + } /*namespace qty*/ +} /*namespace xo*/ + +/** end basis_unit.hpp **/ diff --git a/xo-unit/include/xo/unit/bpu.hpp b/xo-unit/include/xo/unit/bpu.hpp new file mode 100644 index 00000000..29138d94 --- /dev/null +++ b/xo-unit/include/xo/unit/bpu.hpp @@ -0,0 +1,188 @@ +/** @file bpu.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "basis_unit.hpp" +#include "bu_store.hpp" + +namespace xo { + namespace qty { + namespace abbrev { + /** fixed-size string representation for exponent of a basis-power-unit **/ + using power_abbrev_type = flatstring<16>; + + /** @defgroup bpu-abbrev-helpers bpu abbrev helpers **/ + ///@{ + /** @brief construct prefix string for unit exponent + * + * Auxiliary function for @ref bpu_abbrev + **/ + constexpr power_abbrev_type + flatstring_from_exponent(const power_ratio_type & power) + { + if (power.den() == 1) { + if (power.num() == 1) { + /* for no exponent annotation for power ^1 */ + return power_abbrev_type::from_chars(""); + } else { + /* e.g. "^-1", "^2" */ + return (power_abbrev_type::from_flatstring + (flatstring_concat(flatstring("^"), + power_abbrev_type::from_int(power.num())))); + } + } else { + /* e.g. "^1/2", "^-1/2" */ + return (power_abbrev_type::from_flatstring + (flatstring_concat(flatstring("^"), + power.to_str()))); + } + } + + /** construct suffix abbreviation for a basis-power-unit **/ + static constexpr bpu_abbrev_type + bpu_abbrev(dim native_dim, + const scalefactor_ratio_type & scalefactor, + const power_ratio_type & power) + { + return (bpu_abbrev_type::from_flatstring + (flatstring_concat + (bu_abbrev(basis_unit(native_dim, scalefactor)), + flatstring_from_exponent(power)))); + } + ///@} + } + + /** @class bpu + * + * @brief represent product of a compile-time scale-factor with a rational power of a native unit + **/ + template + struct bpu { + public: + using ratio_int_type = Int; + + public: + /** @defgroup bpu-ctors bpu constructors **/ + ///@{ + /** default constructor. creates dimensionless bpu, + * representing zero'th power of sentinel basis unit + **/ + constexpr bpu() = default; + /** construct @c bpu representing exponent @p power of basis unit @p bu **/ + constexpr bpu(const basis_unit & bu, + const power_ratio_type & power) + : bu_{bu}, + power_{power} + {} + /** construct @c bpu representing exponent @p power of @c basis_unit(native_dim,scalefactor) **/ + constexpr bpu(dim native_dim, + const scalefactor_ratio_type & scalefactor, + const power_ratio_type & power) + : bu_(native_dim, scalefactor), + power_{power} + {} + + /** construct bpu representing basis unit @p bu, i.e. with unit exponent **/ + static constexpr bpu unit_power(const basis_unit & bu) { + return bpu(bu, power_ratio_type(1,1)); + } + ///@} + + /** @defgroup bpu-access-methods bpu access methods **/ + ///@{ + /** @brief report this bpu's @ref basis_unit, e.g. @c detail::bu::minute **/ + constexpr const basis_unit & bu() const { return bu_; } + /** @brief report this bpu's @ref dimension, e.g. @c dimension::time **/ + constexpr dimension native_dim() const { return bu_.native_dim(); } + /** @brief report this bpu's scale factor, e.g. @c 60/1 for @c detail::bu::minute **/ + constexpr const scalefactor_ratio_type & scalefactor() const { return bu_.scalefactor(); } + /** @brief report this bpu's exponent, e.g. @c 3/1 for bpu representing cubic meters **/ + constexpr const power_ratio_type & power() const { return power_; } + ///@} + + /** @defgroup bpu-methods **/ + ///@{ + /** abbreviation for this dimension + * + * @code + * bpu(dim::time, + * scalefactor_ratio_type(60,1), + * power_ratio_type(-2,1)).abbrev() => "min^-2" + * @endcode + **/ + constexpr bpu_abbrev_type abbrev() const + { + return abbrev::bpu_abbrev(bu_.native_dim_, + bu_.scalefactor_, + power_); + } + + /** for bpu @c x, @c x.reciprocal() represents dimension of @c 1/x + * + * Example: + * @code + * constexpr auto x = bpu(dim::time, + * scalefactor_ratio_type(60,1), + * power_ratio_type(1)); + * x.abbrev() => "min" + * x.reciprocal().abbrev() => "min^-1" + * @endcode + **/ + constexpr bpu reciprocal() const { + return bpu(bu_.native_dim(), bu_.scalefactor(), power_.negate()); + } + + /** construct bpu representing the same unit, but using @c Int2 to represent exponenct **/ + template + constexpr bpu to_repr() const { + return bpu(this->native_dim(), + this->scalefactor(), + ratio::ratio(power_.num(), power_.den())); + } + ///@} + + public: /* need public members so that a basis_unit instance can be a non-type template parameter (a structural type) */ + /** @defgroup bpu-instance-vars **/ + ///@{ + /** this @c bpu represent a power of basis unit @c bu. + * + * Public to avoid disqualifying @c bpu as a 'structural type'. + **/ + struct basis_unit bu_; + /** this unit represents basis dimension (bu) taken to this power + * + * Public to avoid disqualifying @c bpu as a 'structural type'. + **/ + power_ratio_type power_ = {}; + ///@} + }; + + /** @defgroup bpu-comparison **/ + ///@{ + /** @brief compare bpus @p x and @p y for equality + * + * Equality requires that both basis unit and power are equal + **/ + template + inline constexpr bool + operator==(const bpu & x, const bpu & y) { + return ((x.bu() == y.bu()) + && (x.power_ == y.power_)); + } + + /** @brief compare bpus @p x and @p y for inequality **/ + template + inline constexpr bool + operator!=(const bpu & x, const bpu & y) { + return ((x.bu() != y.bu()) + || (x.power_ != y.power_)); + } + ///@} + + } /*namespace qty*/ +} /*namespace xo*/ + +/** end bpu.hpp **/ diff --git a/xo-unit/include/xo/unit/bpu_iostream.hpp b/xo-unit/include/xo/unit/bpu_iostream.hpp new file mode 100644 index 00000000..41629987 --- /dev/null +++ b/xo-unit/include/xo/unit/bpu_iostream.hpp @@ -0,0 +1,30 @@ +/** @file bpu_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "bpu.hpp" +#include "dim_iostream.hpp" +#include "xo/ratio/ratio_iostream.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + namespace qty { + template + inline std::ostream & + operator<<(std::ostream & os, const bpu & x) { + os << ""; + + return os; + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end bpu_iostream.hpp **/ diff --git a/xo-unit/include/xo/unit/bu_store.hpp b/xo-unit/include/xo/unit/bu_store.hpp new file mode 100644 index 00000000..a06d5b4f --- /dev/null +++ b/xo-unit/include/xo/unit/bu_store.hpp @@ -0,0 +1,279 @@ +/** @file bu_store.hpp **/ + +#pragma once + +//#include "bpu.hpp" +#include "basis_unit.hpp" +#include "xo/ratio/ratio.hpp" +#include +#include + +namespace xo { + namespace qty { + using bpu_abbrev_type = flatstring<24>; + + using power_ratio_type = xo::ratio::ratio; + + namespace detail { + /** @class bu_dim_store + * @brief store basis-unit abbreviations for a particular dimension + **/ + struct bu_dim_store { + /** max number of basis-units per dimension **/ + static constexpr std::size_t max_bu_per_dim = 25; + + /** @defgroup bu-dim-store-type-traits bu-dim-store type traits **/ + ///@{ + using entry_type = std::pair; + + /* e.g. + * [(1/1000000000, "nm"), (1/1000000, "um"), (1/1000, "mm"), (1/1, "m"), (1000/1, "km")] + */ + using native_scale_v = std::array; + ///@} + + public: + constexpr bu_dim_store() = default; + + constexpr bool empty() const { return n_bu_ == 0; } + constexpr std::size_t size() const { return n_bu_; } + + constexpr const entry_type & operator[](std::size_t i) const { return bu_abbrev_v_[i]; } + + /** @brief get least-upper-bound index position in bu_abbrev_v[] + * + * return value in [0, n] where n = .size() + **/ + constexpr std::size_t abbrev_lub_ix(const scalefactor_ratio_type & scalefactor) const + { + if (n_bu_ == 0) + return 0; + + std::size_t lo = 0; + std::size_t hi = n_bu_-1; + + if (scalefactor <= bu_abbrev_v_[lo].first) + return 0; + + auto cmp = (scalefactor <=> bu_abbrev_v_[hi].first); + + if (cmp > 0) + return n_bu_; + + if (cmp == 0) + return hi; + + while (hi-lo > 1) { + /* inv: + * bu_abbrev_v[lo].first < scalefactor <= bu_abbrev_v[hi].first + */ + + std::size_t mid = lo + (hi - lo)/2; + + if (scalefactor > bu_abbrev_v_[mid].first) + lo = mid; + else + hi = mid; + } + + return hi; + } + + constexpr void insert_aux(std::size_t ix, + const entry_type & entry) + { + + if (n_bu_ >= max_bu_per_dim) + return; + + ++n_bu_; + + for (std::size_t dest_ix = n_bu_; dest_ix > ix; --dest_ix) + bu_abbrev_v_[dest_ix] = bu_abbrev_v_[dest_ix - 1]; + + bu_abbrev_v_[ix] = entry; + } + + /** @brief establish abbreviation @p abbrev for basis unit @p bu + **/ + constexpr void bu_establish_abbrev(const scalefactor_ratio_type & scalefactor, + const bu_abbrev_type & abbrev) + { + + std::int32_t i_abbrev = this->abbrev_lub_ix(scalefactor); + + auto entry = std::make_pair(scalefactor, abbrev); + + if ((i_abbrev < bu_abbrev_v_.size()) + && (bu_abbrev_v_[i_abbrev].first == scalefactor)) + { + bu_abbrev_v_[i_abbrev] = entry; + } else { + this->insert_aux(i_abbrev, entry); + } + } + + public: + /** @defgroup bu-dim-store-instance-vars bu-dim-store instance vars **/ + ///@{ + std::size_t n_bu_ = 0; + std::array bu_abbrev_v_; + ///@} + }; /*bu_dim_store*/ + + /** @class bu_store + * @brief associate basis units with abbreviations + **/ + struct bu_store { + /** @defgroup bu-store-constructors bu-store constructors **/ + ///@{ + /** construct canonical instance containing all known basis units **/ + constexpr bu_store() { + // ----- mass ----- + + this->bu_establish_abbrev(detail::bu::picogram, bu_abbrev_type::from_chars("pg")); + this->bu_establish_abbrev(detail::bu::nanogram, bu_abbrev_type::from_chars("ng")); + this->bu_establish_abbrev(detail::bu::microgram, bu_abbrev_type::from_chars("ug")); + this->bu_establish_abbrev(detail::bu::milligram, bu_abbrev_type::from_chars("mg")); + this->bu_establish_abbrev(detail::bu::gram, bu_abbrev_type::from_chars("g")); + this->bu_establish_abbrev(detail::bu::kilogram, bu_abbrev_type::from_chars("kg")); + this->bu_establish_abbrev(detail::bu::tonne, bu_abbrev_type::from_chars("t")); + this->bu_establish_abbrev(detail::bu::kilotonne, bu_abbrev_type::from_chars("kt")); + this->bu_establish_abbrev(detail::bu::megatonne, bu_abbrev_type::from_chars("Mt")); + this->bu_establish_abbrev(detail::bu::gigatonne, bu_abbrev_type::from_chars("Gt")); + + // ----- distance ----- + + this->bu_establish_abbrev(detail::bu::picometer, bu_abbrev_type::from_chars("pm")); + this->bu_establish_abbrev(detail::bu::nanometer, bu_abbrev_type::from_chars("nm")); + this->bu_establish_abbrev(detail::bu::micrometer, bu_abbrev_type::from_chars("um")); + this->bu_establish_abbrev(detail::bu::millimeter, bu_abbrev_type::from_chars("mm")); + this->bu_establish_abbrev(detail::bu::meter, bu_abbrev_type::from_chars("m")); + this->bu_establish_abbrev(detail::bu::kilometer, bu_abbrev_type::from_chars("km")); + this->bu_establish_abbrev(detail::bu::megameter, bu_abbrev_type::from_chars("Mm")); + this->bu_establish_abbrev(detail::bu::gigameter, bu_abbrev_type::from_chars("Gm")); + + this->bu_establish_abbrev(detail::bu::lightsecond, bu_abbrev_type::from_chars("lsec")); + this->bu_establish_abbrev(detail::bu::astronomicalunit, bu_abbrev_type::from_chars("AU")); + + this->bu_establish_abbrev(detail::bu::inch, bu_abbrev_type::from_chars("in")); + this->bu_establish_abbrev(detail::bu::foot, bu_abbrev_type::from_chars("ft")); + this->bu_establish_abbrev(detail::bu::yard, bu_abbrev_type::from_chars("yd")); + this->bu_establish_abbrev(detail::bu::mile, bu_abbrev_type::from_chars("mi")); + + // ----- time ----- + + this->bu_establish_abbrev(detail::bu::picosecond, bu_abbrev_type::from_chars("ps")); + this->bu_establish_abbrev(detail::bu::nanosecond, bu_abbrev_type::from_chars("ns")); + this->bu_establish_abbrev(detail::bu::microsecond, bu_abbrev_type::from_chars("us")); + this->bu_establish_abbrev(detail::bu::millisecond, bu_abbrev_type::from_chars("ms")); + this->bu_establish_abbrev(detail::bu::second, bu_abbrev_type::from_chars("s")); + this->bu_establish_abbrev(detail::bu::minute, bu_abbrev_type::from_chars("min")); + this->bu_establish_abbrev(detail::bu::hour, bu_abbrev_type::from_chars("hr")); + this->bu_establish_abbrev(detail::bu::day, bu_abbrev_type::from_chars("dy")); + this->bu_establish_abbrev(detail::bu::week, bu_abbrev_type::from_chars("wk")); + this->bu_establish_abbrev(detail::bu::month, bu_abbrev_type::from_chars("mo")); + this->bu_establish_abbrev(detail::bu::year250, bu_abbrev_type::from_chars("yr250")); + this->bu_establish_abbrev(detail::bu::year, bu_abbrev_type::from_chars("yr")); + this->bu_establish_abbrev(detail::bu::year360, bu_abbrev_type::from_chars("yr360")); + this->bu_establish_abbrev(detail::bu::year365, bu_abbrev_type::from_chars("yr365")); + + // ----- misc (currency, price) ----- + + this->bu_establish_abbrev(detail::bu::currency, bu_abbrev_type::from_chars("ccy")); + this->bu_establish_abbrev(detail::bu::price, bu_abbrev_type::from_chars("px")); + } + ///@} + + /** @defgroup bu-store-implementation-methods **/ + ///@{ + /** report fallback abbreviation for a basis unit. + * + * Typically unused. Will be invoked only if a basis unit exists for which + * @ref bu_store::bu_establish_abbrev was not called by bu_store's constructor. + * + * Tries to produce something unambiguous, + * while still supplying enough information + * to indicate what's needed to resolve the problem. + * + * Example + * @code + * bu_store.bu_fallback_abbrev(dim::mass, scalefactor_ratio_type(1234,1)) + * => "1234g" + * @endcode + **/ + static constexpr bu_abbrev_type + bu_fallback_abbrev(dim basis_dim, + const scalefactor_ratio_type & scalefactor) + { + return (bu_abbrev_type::from_flatstring + (flatstring_concat + (scalefactor.to_str(), + native_unit2_v[static_cast(basis_dim)].abbrev_str()))); + } + ///@} + + /** @defgroup bu-store-access-methods **/ + ///@{ + /** @brief get basis-unit abbreviation at runtime **/ + constexpr bu_abbrev_type bu_abbrev(const basis_unit & bu) const + { + const auto & bu_abbrev_v = bu_abbrev_vv_[static_cast(bu.native_dim())]; + + std::size_t i_abbrev = bu_abbrev_v.abbrev_lub_ix(bu.scalefactor()); + + if ((i_abbrev < bu_abbrev_v.size()) + && (bu_abbrev_v[i_abbrev].first == bu.scalefactor())) + { + return bu_abbrev_v[i_abbrev].second; + } else { + return bu_fallback_abbrev(bu.native_dim(), bu.scalefactor()); + } + } + ///@} + + /** @addtogroup bu-store-implementation-methods **/ + ///@{ + /** associate abbreviation @p abbrev with basis unit @p bu + * + * Example: + * @code + * // femtograms + * bu_store.bu_establish_abbrev(detail::bu::mass_unit(1, 1000000000000000), "fg") + * @endcode + **/ + constexpr void bu_establish_abbrev(const basis_unit & bu, + const bu_abbrev_type & abbrev) { + auto & dim_store = bu_abbrev_vv_[static_cast(bu.native_dim_)]; + + dim_store.bu_establish_abbrev(bu.scalefactor_, abbrev); + } + ///@} + + public: /* ntoe: public members required so bu_dim_store can be a structural type */ + /** @defgroup bu-store-instance-vars **/ + ///@{ + /** bu-store contents, indexed by native dimension **/ + std::array bu_abbrev_vv_; + ///@} + }; + } /*namespace detail*/ + + /** @brief global abbreviation store. + * + * @note + * Extending the contents of this store at runtime is not supported, + * in favor of preserving constexpr abbreviations. + **/ + static constexpr detail::bu_store bu_abbrev_store; + + /** @brief get abbreviation for basis-unit @p bu **/ + constexpr bu_abbrev_type + bu_abbrev(const basis_unit & bu) + { + return bu_abbrev_store.bu_abbrev(bu); + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end bu_store.hpp **/ diff --git a/xo-unit/include/xo/unit/dim_iostream.hpp b/xo-unit/include/xo/unit/dim_iostream.hpp new file mode 100644 index 00000000..0621578b --- /dev/null +++ b/xo-unit/include/xo/unit/dim_iostream.hpp @@ -0,0 +1,21 @@ +/** @file dim_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "dimension.hpp" +#include + +namespace xo { + namespace qty { + inline std::ostream & + operator<<(std::ostream & os, dim x) { + os << dim2str(x); + return os; + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end dim_iostream.hpp **/ diff --git a/xo-unit/include/xo/unit/dimension.hpp b/xo-unit/include/xo/unit/dimension.hpp new file mode 100644 index 00000000..c28936e2 --- /dev/null +++ b/xo-unit/include/xo/unit/dimension.hpp @@ -0,0 +1,65 @@ +/* @file dimension.hpp */ + +#pragma once + +#include + +namespace xo { + namespace qty { + /** @enum dimension + * @brief represent an abstract dimension. + * + * *xo-unit* units are expressed as a cartesian product + * of powers of these dimensions. + **/ + enum class dimension { + /** sentinel value. not a dimension **/ + invalid = -1, + + /** weight. native unit = 1 gram **/ + mass, + /** distance. native unit = 1 meter **/ + distance, + /** time. native unit = 1 second **/ + time, + /** a currency amount. native unit depends on actual currency. + * For USD: one US dollar. + * + * NOTE: multicurrency work not supported by *xo-unit*. + * - (1usd + 1eur) is well-defined. + * - (1sec + 1m) is not. + **/ + currency, + /** A screen price. + * The interpretation of prices is highly context dependent; + * expect useful to bucket separately from currenty amounts. + **/ + price, + + /** not a dimension. comes last, counts entries **/ + n_dim + }; + + using dim = dimension; + + /** @brief string value for a dimension enum **/ + inline const char * + dim2str(dimension x) + { + switch(x) { + case dimension::mass: return "mass"; + case dimension::distance: return "distance"; + case dimension::time: return "time"; + case dimension::currency: return "currency"; + case dimension::price: return "price"; + default: break; + } + return "?dim"; + } + + /** @brief number of built-in dimensions, convenient for array sizing **/ + static constexpr std::size_t n_dim = static_cast(dimension::n_dim); + } /*namespace qty*/ +} /*namespace xo*/ + +/* end dimension.hpp */ diff --git a/xo-unit/include/xo/unit/native_unit.hpp b/xo-unit/include/xo/unit/native_unit.hpp new file mode 100644 index 00000000..e6dded14 --- /dev/null +++ b/xo-unit/include/xo/unit/native_unit.hpp @@ -0,0 +1,49 @@ +/** @file native_unit.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "dimension.hpp" +#include "xo/flatstring/flatstring.hpp" + +namespace xo { + namespace qty { + using native_unit2_abbrev_type = flatstring<8>; + + /** @class native_unit + * + * @brief Represent a native (built-in) unit. + * + * A basis_unit is expressed as a multiple of a native_unit + * + **/ + struct native_unit { + public: + constexpr native_unit(dimension native_dim, + const native_unit2_abbrev_type & abbrev_str) + : native_dim_{native_dim}, + abbrev_str_{abbrev_str} + {} + + constexpr dimension native_dim() const { return native_dim_; } + constexpr const native_unit2_abbrev_type & abbrev_str() const { return abbrev_str_; } + + private: + dimension native_dim_; + native_unit2_abbrev_type abbrev_str_; + }; + + static constexpr native_unit native_unit2_v[n_dim] = { + native_unit(dimension::mass, native_unit2_abbrev_type::from_chars("g")), + native_unit(dimension::distance, native_unit2_abbrev_type::from_chars("m")), + native_unit(dimension::time, native_unit2_abbrev_type::from_chars("s")), + native_unit(dimension::currency, native_unit2_abbrev_type::from_chars("ccy")), + native_unit(dimension::price, native_unit2_abbrev_type::from_chars("px")), + }; + + } /*namespace qty*/ +} /*namespace xo*/ + +/** end native_unit.hpp **/ diff --git a/xo-unit/include/xo/unit/natural_unit.hpp b/xo-unit/include/xo/unit/natural_unit.hpp new file mode 100644 index 00000000..a03c73e4 --- /dev/null +++ b/xo-unit/include/xo/unit/natural_unit.hpp @@ -0,0 +1,527 @@ +/** @file natural_unit.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "bpu.hpp" +#include +#include + +namespace xo { + namespace qty { + using nu_abbrev_type = flatstring<32>; + + template + class natural_unit; + + namespace detail { + template + constexpr void + push_bpu_array(natural_unit * p_target, Ts... args); + + template + constexpr void + push_bpu_array(natural_unit * p_target) {} + + template + constexpr void + push_bpu_array(natural_unit * p_target, T0 && bpu0, Ts... args) { + p_target->push_back(bpu0); + push_bpu_array(p_target, args...); + } + + template + struct nu_maker { + template + static constexpr natural_unit + make_nu(Ts... args) { + natural_unit bpu_array; + detail::push_bpu_array(&bpu_array, args...); + return bpu_array; + } + }; + } + + /** @class natural_unit + * @brief an array representing the cartesian product of distinct basis-power-units + * + * 1. Quantities are represented as a multiple of a natural unit + * 2. Each bpu in the array represents a power of a basis dimension, e.g. "meter" or "second^2". + * 3. Each bpu in an array has a different dimension id. + * For example @c dim::time, if present, appears once. + * 4. Basis dimensions can appear in any order. + * Order used for constructing abbreviations: will get @c "kg.m" or @c "m.kg" + * depending on the ordering of @c dim::distance and @c dim::mass in @c bpu_v_ + * + * @c Int supplies representation for numerator and denominator in basis-unit scale factors. + **/ + template + class natural_unit { + public: + /** @defgroup natural-unit-type-traits natural unit type traits **/ + ///@{ + /** @brief representation for numerator and denominator of scalefactor ratios **/ + using ratio_int_type = Int; + ///@} + + public: + /** @addtogroup natural-unit-ctors **/ + ///@{ + + /** construct dimensionless unit **/ + constexpr natural_unit() : n_bpu_{0} {} + + /** construct unit representing basis unit @p bu with exponent @p power **/ + static constexpr natural_unit from_bu(basis_unit bu, + power_ratio_type power = power_ratio_type(1)) { + return detail::nu_maker::make_nu(bpu(bu, power)); + } + + ///@} + + /** @addtogroup natural-unit-access-methods **/ + ///@{ + + /** always true. Provided for symmetry with @c xo::qty::scaled_unit::is_natural **/ + constexpr bool is_natural() const { return true; } + + /** get member @c n_bpu **/ + constexpr std::size_t n_bpu() const { return n_bpu_; } + /** true if this unit has no dimension **/ + constexpr bool is_dimensionless() const { return n_bpu_ == 0; } + + /** get address of member @c bpu_v **/ + constexpr bpu * bpu_v() const { return bpu_v_; } + + ///@} + + /** @defgroup natural-unit-methods **/ + ///@{ + + /** construct reciprocal of this unit. + * + * For example reciprocal of a newton (abbreviation @c "kg.m.s^-2") is + * a unit with abbreviation @c "kg^-1.m^-1.s^2" + **/ + constexpr natural_unit reciprocal() const { + natural_unit retval; + + for (std::size_t i = 0; i < this->n_bpu(); ++i) + retval.push_back((*this)[i].reciprocal()); + + return retval; + } + + /** abbreviation for this unit. + * + * Apply as suffix when printing quantities involving this unit. + * + * For example @c "mm" for millimeters, or @c "ns" for nanoseconds + **/ + constexpr nu_abbrev_type abbrev() const { + nu_abbrev_type retval; + + for (std::size_t i = 0; i < n_bpu_; ++i) { + if (i > 0) + retval.append("."); + retval.append(bpu_v_[i].abbrev(), 0, -1); + } + + return retval; + } + + /** remove bpu at position @p p **/ + constexpr void remove_bpu(size_t p) { + for (std::size_t i = p; i+1 < n_bpu_; ++i) + bpu_v_[i] = bpu_v_[i+1]; + + --n_bpu_; + } + + /** append @p bpu to this unit in-place + * + * Require @c bpu.native_dim does not match any existing member of @ref bpu_v_ + **/ + constexpr void push_back(const bpu & bpu) { + if (n_bpu_ < n_dim) + bpu_v_[n_bpu_++] = bpu; + } + + ///@} + + /** @addtogroup natural-unit-access-methods **/ + ///@{ + + /** get bpu for dimension @p d. if d isn't present, construct bpu with 0 power **/ + constexpr bpu lookup_dim(dimension d) const { + for (std::size_t i = 0, n = n_bpu(); i(d, scalefactor_ratio_type(0), power_ratio_type(0)); + } + + /** get element @p i of @ref bpu_v_ **/ + constexpr bpu & operator[](std::size_t i) { return bpu_v_[i]; } + /** get element @p i of @ref bpu_v_ (const version) **/ + constexpr const bpu & operator[](std::size_t i) const { return bpu_v_[i]; } + + ///@} + + /** @defgroup natural-unit-conversion-methods **/ + ///@{ + + /** convert to equivalent unit using scalefactor representation @p Int2 instead of + * @ref ratio_int_type + **/ + template + constexpr natural_unit to_repr() const { + natural_unit retval; + + std::size_t i = 0; + for (; i < n_bpu_; ++i) + retval.push_back(bpu_v_[i].template to_repr()); + + return retval; + } + + ///@} + + public: /* public members so instance can be non-type template parameter (is a structural type) */ + /** @defgroup natural-unit-instance-vars **/ + ///@{ + + /** the number of occupied slots in @c bpu_v_ **/ + std::size_t n_bpu_; + + /** storage for basis power units **/ + bpu bpu_v_[n_dim]; + + ///@} + }; + + /** @defgroup natural-unit-comparison-functions natural-unit comparison functions **/ + ///@{ + + /** compare natural units @p x, @p y for equality. **/ + template + constexpr bool + operator==(const natural_unit & x, + const natural_unit & y) + { + if (x.n_bpu() != y.n_bpu()) + return false; + + /* does x contain any dimension that isn't present in y? */ + for (std::size_t i = 0, n = x.n_bpu(); i & xi = x[i]; + if (xi != y.lookup_dim(xi.native_dim())) + return false; + } + + /* if all bpu's x[i] match something from y, then x,y must be equal + * since they each have the same number of bpu's + */ + + return true; + } + + /** compare natural units @p x, @p y for inequality **/ + template + constexpr bool + operator!=(const natural_unit & x, + const natural_unit & y) { + return !(x == y); + } + + ///@} + + namespace detail { + /** + * Given bpu ~ (b.u)^p: + * - b = bpu.scalefactor + * - u = bpu.native_dim + * - p = bpu.power + * + * want to rewrite in the form a'.(b'.u)^p + * + * Can compute a' exactly iff p is integral. + * In that case: + * (b.u)^p = ((b/b').b'.u)^p + * = (b/b')^p.(b'.u)^p + * = a'.(b'.u)^p with a' = (b/b')^p + * + * Can write p = p0 + q, with p0 = floor(p) integral, q = frac(p) in [0,1) + * + * Then + * (b/b')^p = (b/b')^p0 * (b/b')^q + * + * we'll compute: + * - (b/b')^p0 exactly (as a ratio) + * - (b/b')^q inexactly (as a double) + **/ + + template > + struct outer_scalefactor_result { + constexpr outer_scalefactor_result(const OuterScale & outer_scale_factor, + double outer_scale_sq) + : outer_scale_factor_{outer_scale_factor}, + outer_scale_sq_{outer_scale_sq} {} + + /* (b/b')^p0 */ + OuterScale outer_scale_factor_; + /* (b/b')^q -- until c++26 only allow q=0 or q=1/2 */ + double outer_scale_sq_; + }; + + template > + struct bpu2_rescale_result { + constexpr bpu2_rescale_result(const bpu & bpu_rescaled, + const OuterScale & outer_scale_factor, + double outer_scale_sq) + : bpu_rescaled_{bpu_rescaled}, + outer_scale_factor_{outer_scale_factor}, + outer_scale_sq_{outer_scale_sq} + {} + + /* (b'.u)^p */ + bpu bpu_rescaled_; + /* (b/b')^p0 */ + OuterScale outer_scale_factor_; + /* [(b/b')^q]^2 -- until c++26 only allow q=0 or q=1/2 */ + double outer_scale_sq_; + }; + + template < typename Int, + typename OuterScale = ratio::ratio > + constexpr + bpu2_rescale_result + bpu2_rescale(const bpu & orig, + const scalefactor_ratio_type & new_scalefactor) + { + /* we have orig, representing qty (b.u)^p, + * with b=orig.scalefactor, u=native dimension, p=orig.power + */ + + ratio::ratio mult = (orig.scalefactor() / new_scalefactor); + + /* inv: p_frac in (-1, 1) */ + auto p_frac = orig.power().frac(); + + /* asof c++26: replace mult_sq with ::pow(mult, p_frac) */ + double mult_sq = std::numeric_limits::quiet_NaN(); + + /* pre-c++26 workaround */ + { + if (p_frac.den() == 1) { + mult_sq = 1.0; + } else if(p_frac.num() == 1 && p_frac.den() == 2) { + mult_sq = mult.template convert_to(); + } else if(p_frac.num() == -1 && p_frac.den() == 2) { + mult_sq = 1.0 / mult.template convert_to(); + } else { + // remaining possibilities not supported until c++26 + } + } + + ratio::ratio mult_p = mult.power(orig.power().floor()); + + return bpu2_rescale_result(bpu(orig.native_dim(), + new_scalefactor, + orig.power()), + mult_p.template convert_to(), + mult_sq); + } + + template < typename Int, + typename OuterScale > + constexpr + outer_scalefactor_result + bpu_product_inplace(bpu * p_target_bpu, + const bpu & rhs_bpu_orig) + { + assert(rhs_bpu_orig.native_dim() == p_target_bpu->native_dim()); + + bpu2_rescale_result rhs_bpu_rr + = bpu2_rescale(rhs_bpu_orig, + p_target_bpu->scalefactor().template convert_to()); + + *p_target_bpu = bpu(p_target_bpu->native_dim(), + p_target_bpu->scalefactor(), + p_target_bpu->power() + rhs_bpu_orig.power()); + + return outer_scalefactor_result(rhs_bpu_rr.outer_scale_factor_, + rhs_bpu_rr.outer_scale_sq_); + } + + template < typename Int, + typename OuterScale > + constexpr + outer_scalefactor_result + bpu_ratio_inplace(bpu * p_target_bpu, + const bpu & rhs_bpu_orig) + { + assert(rhs_bpu_orig.native_dim() == p_target_bpu->native_dim()); + + bpu2_rescale_result rhs_bpu_rr + = bpu2_rescale(rhs_bpu_orig, + p_target_bpu->scalefactor()); + + *p_target_bpu = bpu(p_target_bpu->native_dim(), + p_target_bpu->scalefactor(), + p_target_bpu->power() - rhs_bpu_orig.power()); + + return outer_scalefactor_result + (OuterScale(1) / rhs_bpu_rr.outer_scale_factor_, + 1.0 / rhs_bpu_rr.outer_scale_sq_); + } + + template < typename Int, + typename OuterScale > + constexpr + outer_scalefactor_result + nu_product_inplace(natural_unit * p_target, + const bpu & bpu) + { + std::size_t i = 0; + for (; i < p_target->n_bpu(); ++i) { + auto * p_target_bpu = &((*p_target)[i]); + + if (p_target_bpu->native_dim() == bpu.native_dim()) { + outer_scalefactor_result retval + = bpu_product_inplace(p_target_bpu, bpu); + + if (p_target_bpu->power().is_zero()) { + /* dimension assoc'd with *p_target_bpu has been cancelled */ + p_target->remove_bpu(i); + } + + return retval; + } + } + + /* control here: i=p_target->n_bpu() + * Dimension represented by bpu does not already appear in *p_target. + * Adopt bpu's scalefactor + */ + + p_target->push_back(bpu); + + return outer_scalefactor_result + (OuterScale(1) /*outer_scale_factor*/, + 1.0 /*outer_scale_sq*/); + } + + template < typename Int, + typename OuterScale = ratio::ratio > + constexpr + outer_scalefactor_result + nu_ratio_inplace(natural_unit * p_target, + const bpu & bpu) + { + std::size_t i = 0; + for (; i < p_target->n_bpu(); ++i) { + auto * p_target_bpu = &((*p_target)[i]); + + if (p_target_bpu->native_dim() == bpu.native_dim()) { + outer_scalefactor_result retval + = bpu_ratio_inplace(p_target_bpu, bpu); + + if (p_target_bpu->power().is_zero()) { + /* dimension assoc'd with *p_target_bpu has been cancelled */ + p_target->remove_bpu(i); + } + + return retval; + } + } + + /* here: i=p_target->n_bpu() + * Dimension represented by bpu does not already appear in *p_target. + * Adopt bpu's scalefactor + */ + + p_target->push_back(bpu.reciprocal()); + + return outer_scalefactor_result + (OuterScale(1) /*outer_scale_factor*/, + 1.0 /*outer_scale_sq*/); + } + + } /*namespace detail*/ + + /** @brief namespace for constants representing basis natural units + * + * Application code will typically use instead parallel scaled-unit constants + * (see the 'u' namespace in 'scaled_unit.hpp') + **/ + namespace nu { + constexpr auto dimensionless = natural_unit(); + + // ----- mass ----- + + constexpr auto picogram = natural_unit::from_bu(detail::bu::picogram); + constexpr auto nanogram = natural_unit::from_bu(detail::bu::nanogram); + constexpr auto microgram = natural_unit::from_bu(detail::bu::microgram); + constexpr auto milligram = natural_unit::from_bu(detail::bu::milligram); + constexpr auto gram = natural_unit::from_bu(detail::bu::gram); + constexpr auto kilogram = natural_unit::from_bu(detail::bu::kilogram); + constexpr auto tonne = natural_unit::from_bu(detail::bu::tonne); + constexpr auto kilotonne = natural_unit::from_bu(detail::bu::kilotonne); + constexpr auto megatonne = natural_unit::from_bu(detail::bu::megatonne); + constexpr auto gigatonne = natural_unit::from_bu(detail::bu::gigatonne); + + // ----- distance ----- + + constexpr auto picometer = natural_unit::from_bu(detail::bu::picometer); + constexpr auto nanometer = natural_unit::from_bu(detail::bu::nanometer); + constexpr auto micrometer = natural_unit::from_bu(detail::bu::micrometer); + constexpr auto millimeter = natural_unit::from_bu(detail::bu::millimeter); + constexpr auto meter = natural_unit::from_bu(detail::bu::meter); + constexpr auto kilometer = natural_unit::from_bu(detail::bu::kilometer); + constexpr auto megameter = natural_unit::from_bu(detail::bu::megameter); + constexpr auto gigameter = natural_unit::from_bu(detail::bu::gigameter); + constexpr auto lightsecond = natural_unit::from_bu(detail::bu::lightsecond); + constexpr auto astronomicalunit = natural_unit::from_bu(detail::bu::astronomicalunit); + + constexpr auto inch = natural_unit::from_bu(detail::bu::inch); + constexpr auto foot = natural_unit::from_bu(detail::bu::foot); + constexpr auto yard = natural_unit::from_bu(detail::bu::yard); + constexpr auto mile = natural_unit::from_bu(detail::bu::mile); + + // ----- time ----- + + constexpr auto picosecond = natural_unit::from_bu(detail::bu::picosecond); + constexpr auto nanosecond = natural_unit::from_bu(detail::bu::nanosecond); + constexpr auto microsecond = natural_unit::from_bu(detail::bu::microsecond); + constexpr auto millisecond = natural_unit::from_bu(detail::bu::millisecond); + constexpr auto second = natural_unit::from_bu(detail::bu::second); + constexpr auto minute = natural_unit::from_bu(detail::bu::minute); + constexpr auto hour = natural_unit::from_bu(detail::bu::hour); + constexpr auto day = natural_unit::from_bu(detail::bu::day); + constexpr auto week = natural_unit::from_bu(detail::bu::week); + constexpr auto month = natural_unit::from_bu(detail::bu::month); + constexpr auto year = natural_unit::from_bu(detail::bu::year); + constexpr auto year250 = natural_unit::from_bu(detail::bu::year250); + constexpr auto year360 = natural_unit::from_bu(detail::bu::year360); + constexpr auto year365 = natural_unit::from_bu(detail::bu::year365); + + constexpr auto currency = natural_unit::from_bu(detail::bu::currency); + + constexpr auto price = natural_unit::from_bu(detail::bu::price); + + constexpr auto volatility_30d = natural_unit::from_bu(detail::bu::month, power_ratio_type(-1,2)); + constexpr auto volatility_250d = natural_unit::from_bu(detail::bu::year250, power_ratio_type(-1,2)); + constexpr auto volatility_360d = natural_unit::from_bu(detail::bu::year360, power_ratio_type(-1,2)); + constexpr auto volatility_365d = natural_unit::from_bu(detail::bu::year365, power_ratio_type(-1,2)); + } /*namespace nu*/ + } /*namespace qty*/ +} /*namespace xo*/ + +/** end natural_unit.hpp **/ diff --git a/xo-unit/include/xo/unit/natural_unit_iostream.hpp b/xo-unit/include/xo/unit/natural_unit_iostream.hpp new file mode 100644 index 00000000..aac612e3 --- /dev/null +++ b/xo-unit/include/xo/unit/natural_unit_iostream.hpp @@ -0,0 +1,29 @@ +/** @file natural_unit_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "natural_unit.hpp" +#include "bpu_iostream.hpp" +#include + +namespace xo { + namespace qty { + template + inline std::ostream & + operator<<(std::ostream & os, const natural_unit & x) { + os << " 0) + os << ", "; + os << x[i]; + } + os << "]>"; + return os; + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end natural_unit_iostream.hpp **/ diff --git a/xo-unit/include/xo/unit/numeric_concept.hpp b/xo-unit/include/xo/unit/numeric_concept.hpp new file mode 100644 index 00000000..b853e8f5 --- /dev/null +++ b/xo-unit/include/xo/unit/numeric_concept.hpp @@ -0,0 +1,34 @@ +/* @file numeric_concept.hpp */ + +#pragma once + +#include + +namespace xo { + namespace qty { + /** @concept numeric_concept + * @brief Concept for values that participate in arithmetic operations (+,-,*,/) and comparisons + * + * Intended to include at least: + * - built-in integral and floating-point types + * - xo::raio + * - xo::unit::quantity + * + * Intend numeric_concept to apply to types suitable for + * xo::unit::quantity::repr_type. + **/ + template + concept numeric_concept = requires(T x, U y) + { + { -x }; + { x - y }; + { x + y }; + { x * y }; + { x / y }; + { x == y }; + { x != y }; + }; + } /*namespace qty*/ +} /*namespace xo*/ + +/* end numeric_concept.hpp */ diff --git a/xo-unit/include/xo/unit/quantity.hpp b/xo-unit/include/xo/unit/quantity.hpp new file mode 100644 index 00000000..4a232b5b --- /dev/null +++ b/xo-unit/include/xo/unit/quantity.hpp @@ -0,0 +1,1018 @@ +/** @file quantity.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "quantity_ops.hpp" +#include "natural_unit.hpp" +#include "scaled_unit.hpp" +#include "scaled_unit_concept.hpp" + +namespace xo { + namespace qty { + /** @class quantity + * + * @brief represent a scalar quantity with associated units. + * + * - @p NaturalUnit is a non-type template paramoeter + * identifying a unit used for this quantity. + * In *xo-unit* it will be an instance of @c natural_unit + * - @p Repr is a type used to represent a multiple + * of @p NaturalUnit. + * + * Enforce dimensional consistency at compile time. + * sizeof(quantity) == sizeof(Repr). + * + * A quantity's runtime state consists of exactly one @p Repr instance: + * @code + * sizeof(quantity) == sizeof(Repr) + * @endcode + **/ + template < + auto ScaledUnit, + typename Repr = double> + requires (ScaledUnit.is_natural() && ScaledUnit.is_scaled_unit_type()) + class quantity { + public: + /** @defgroup quantity-type-traits quantity type traits **/ + ///@{ + /** @brief runtime representation for value of this type **/ + using repr_type = Repr; + /** @brief type used to represent unit information */ + using unit_type = decltype(ScaledUnit); + /** @brief type used for numerator and denominator in basis-unit scalefactor ratios */ + using ratio_int_type = unit_type::ratio_int_type; + /** @brief double-width type used for numerator and denominator of intermediate + * scalefactor ratios. Used to mitigate loss of precision during computation + * of conversion factors between units with widely-differing magnitude + **/ + using ratio_int2x_type = detail::width2x_t; + ///@} + + public: + /** @defgroup quantity-ctors quantity constructors**/ + ///@{ + /** @brief create a zero amount with dimension @c ScaledUnit **/ + constexpr quantity() : scale_{0} {} + /** @brief create a quantity representing @p scale @c ScaledUnits **/ + explicit constexpr quantity(Repr scale) : scale_{scale} {} + ///@} + + /** @defgroup quantity-constants static quantity constants **/ + ///@{ + /** @brief Use to distinguish @ref quantity from xquantity instances. + * + * Useful in c++ template resolution. + **/ + static constexpr bool always_constexpr_unit = true; + ///@} + + /** @defgroup quantity-access-methods quantity access methods **/ + ///@{ + + /** @brief value of @c scale_ in quantity representing amount (@c scale_ * @c s_unit) **/ + constexpr const repr_type & scale() const { return scale_; } + + /** @brief s_unit in quantity representing amount (@c scale_ * @c s_unit) **/ + constexpr const unit_type & unit() const { return s_scaled_unit; } + + /** @brief true iff this quantity is strictly negative **/ + constexpr bool is_negative() const { return scale_ < Repr{0}; } + /** @brief true iff this quantity is strictly positive **/ + constexpr bool is_positive() const { return scale_ > Repr{0}; } + + /** @brief true iff this quantity represents a dimensionless value **/ + static constexpr bool is_dimensionless() { + return s_scaled_unit.is_dimensionless(); + } + + /** abbreviated suffix for quantities with this unit **/ + constexpr nu_abbrev_type abbrev() const { + return s_scaled_unit.natural_unit_.abbrev(); + } + + ///@} + + /** @defgroup quantity-arithmetic-support **/ + ///@{ + + /** create unit quantity with same unit as @c this **/ + constexpr + auto unit_qty() const { + return quantity(1); + } + + /** create zero quantity with same unit as @c this **/ + constexpr + auto zero_qty() const { + return quantity(0); + } + + constexpr + auto reciprocal() const { + return quantity(1.0 / scale_); + } + ///@} + + /** @defgroup quantity-unit-conversion **/ + ///@{ + + /** create equivalent quantity using scale representation @p Repr2 instead of @c Repr **/ + template + constexpr + auto with_repr() const { + return quantity(scale_); + } + + /** create equivalent quantity expressed as a multiple of @p NaturalUnit2 + * instead of @ref s_unit + **/ + template NaturalUnit2> + constexpr + auto rescale() const { + /* conversion factor from .unit -> unit2*/ + auto rr = detail::su_ratio(s_scaled_unit.natural_unit_, + NaturalUnit2); + + if (rr.natural_unit_.is_dimensionless()) { + repr_type r_scale = (((rr.outer_scale_sq_ == 1.0) + ? 1.0 + : ::sqrt(rr.outer_scale_sq_)) + * rr.outer_scale_factor_.template convert_to() + * this->scale_); + return quantity(r_scale); + } else { + return quantity(std::numeric_limits::quiet_NaN()); + } + } + + /** create equivalent quantity expressed as as multiple of @p ScaledUnit2 + * instead of @ref s_unit + **/ + template ScaledUnit2> + constexpr + auto rescale_ext() const { + /* conversion factor from .unit -> unit2*/ + auto rr = detail::su_ratio(s_scaled_unit.natural_unit_, + ScaledUnit2.natural_unit_); + + if (rr.natural_unit_.is_dimensionless()) { + /* NOTE: test for unit .outer_scale_sq values to get constexpr result with c++23 + * and integer dimension powers. + * + * NOTE: we don't intend to support mixed-unit quantities. + * If we change intention, will need to take into account + * (s_scaled_unit.outer_scale_factor_, s_scaled_unit.outer_scale_sq_) + */ + repr_type r_scale = ((((rr.outer_scale_sq_ == 1.0) + && (ScaledUnit2.outer_scale_sq_ == 1.0)) + ? 1.0 + : ::sqrt(rr.outer_scale_sq_ / ScaledUnit2.outer_scale_sq_)) + * rr.outer_scale_factor_.template convert_to() + * this->scale_ + / ScaledUnit2.outer_scale_factor_.template convert_to()); + return quantity(r_scale); + } else { + return quantity(std::numeric_limits::quiet_NaN()); + } + } + ///@} + + /** @addtogroup quantity-arithmetic-support **/ + ///@{ + + /** create quantity representing this amount multiplied by dimensionless value @p x + * + * @pre x must be an arithmetic type such as @c int or @c double + **/ + template + requires std::is_arithmetic_v + constexpr auto scale_by(Dimensionless x) const { + using r_repr_type = std::common_type_t; + + return quantity(x * this->scale_); + } + + /** create quantity representing this quantity divided by dimensionless value @p x + * + * @pre x must be an arithmetic type such as @c int or @c double + **/ + template + requires std::is_arithmetic_v + constexpr auto divide_by(Dimensionless x) const { + using r_repr_type = std::common_type_t; + + return quantity(this->scale_ / x); + } + + /** create quantity representing dimensionless value @p x divided by this quantity + * + * @pre x must be an arithmetic type such as @c int or @c double + **/ + template + requires std::is_arithmetic_v + constexpr auto divide_into(Dimensionless x) const { + using r_repr_type = std::common_type_t; + + return quantity + (static_cast(x) / this->scale_); + } + + ///@} + + /** @defgroup quantity-comparison-support **/ + ///@{ + + /** compare two @c quantity instances, under three-way comparison **/ + template + static constexpr + auto compare(const quantity &x, const Quantity2 & y) { + quantity y2 = y.template rescale_ext(); + + return x.scale() <=> y2.scale(); + } + + ///@} + + /** @defgroup quantity-operators **/ + ///@{ + + /** unary negation; preserves unit information **/ + quantity operator-() const { + return quantity(-scale_); + } + + /** add @p y in-place, converting units if necessary **/ + template + constexpr + quantity & operator+=(const Quantity2 & y) { + quantity y2 = y.template rescale_ext(); + + this->scale_ += y2.scale(); + + return *this; + } + + /** subtract @p y in-place, converting units if necessary **/ + template + constexpr + quantity & operator-=(const Quantity2 & y) { + quantity y2 = y.template rescale_ext(); + + this->scale_ -= y2.scale(); + + return *this; + } + + /** multiply @p y in-place. y must be dimensionless **/ + template + requires std::is_arithmetic_v + constexpr + quantity & operator*=(Dimensionless y) { + this->scale_ *= y; + return *this; + } + + /** divide @p y in-place. y must be dimensionless **/ + template + requires std::is_arithmetic_v + constexpr + quantity & operator/=(Dimensionless y) { + this->scale_ /= y; + return *this; + } + + ///@} + + /** @defgroup quantity-assignment quantity assignment operators **/ + ///@{ + + /** assignment from quantity with identical units **/ + quantity & operator=(const quantity & x) { + this->scale_ = x.scale_; + return *this; + } + + /** assignment from quantity with compatible units **/ + template + requires(quantity_concept + && Q2::always_constexpr_unit) + quantity & operator=(const Q2 & x) { + auto x2 = x.template rescale_ext(); + + this->scale_ = x2.scale(); + + return *this; + } + + ///@} + + /** @defgroup quantity-unit-conversion **/ + ///@{ + + /** */ + template + requires(quantity_concept + && Q2::always_constexpr_unit) + constexpr operator Q2() const { + return this->template rescale_ext().template with_repr(); + } + + /** For dimensionless quantities: convert to underlying scale value + * + * Not present for dimensioned quantities. + **/ + constexpr operator Repr() const + requires (ScaledUnit.is_dimensionless()) + { + return scale_; + } + + ///@} + + public: /* need public members so that instance can be a non-type template parameter (is a structural type) */ + /** @defgroup quantity-static-vars **/ + ///@{ + + /** @brief unit for quantity of this type. Determined at compile-time **/ + static constexpr scaled_unit s_scaled_unit = ScaledUnit; + + ///@} + + /** @defgroup quantity-instance-vars **/ + ///@{ + + /** quantity represents this multiple of @ref s_scaled_unit + * + * Public to avoid disqualifying @c quantity as a 'structural type'; + * prerequisite for using a @c quantity instance as a non-type template parameter + **/ + Repr scale_ = Repr{}; + + ///@} + }; + + ///@{ + + /** + * + **/ + template + constexpr auto + rescale(const Quantity & x, + const scaled_unit & su) { + return x.template rescale(); + } + + ///@} + + namespace detail { + struct quantity_util { + /* parallel implementation to xquantity multiply, + * but return type will have dimension computed at compile-time + */ + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + static constexpr auto multiply(Q1 x, Q2 y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + constexpr auto rr = detail::su_product(x.unit().natural_unit_, + y.unit().natural_unit_); + + r_repr_type r_scale = (((rr.outer_scale_sq_ == 1.0) + ? 1.0 + : ::sqrt(rr.outer_scale_sq_)) + * rr.outer_scale_factor_.template convert_to() + * static_cast(x.scale()) + * static_cast(y.scale())); + + return quantity(rr.natural_unit_), + r_repr_type>(r_scale); + } + + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + static constexpr auto divide(Q1 x, Q2 y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + constexpr auto rr = detail::su_ratio(x.unit().natural_unit_, + y.unit().natural_unit_); + + r_repr_type r_scale = (((rr.outer_scale_sq_ == 1.0) + ? 1.0 + : ::sqrt(rr.outer_scale_sq_)) + * rr.outer_scale_factor_.template convert_to() + * static_cast(x.scale()) + / static_cast(y.scale())); + + return quantity(rr.natural_unit_), + r_repr_type>(r_scale); + } + + template + requires(quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + static constexpr auto add(Q1 x, Q2 y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + /* conversion to get y in same units as x: multiply by y/x */ + auto rr = detail::su_ratio(y.unit().natural_unit_, + x.unit().natural_unit_); + + if (rr.natural_unit_.is_dimensionless()) { + r_repr_type r_scale = (static_cast(x.scale()) + + (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(y.scale()))); + + return quantity(r_scale); + } else { + /* units don't match! */ + return quantity(std::numeric_limits::quiet_NaN()); + } + } + + template + requires(quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + static constexpr auto subtract(Q1 x, Q2 y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + /* conversion to get y in same units as x: multiply by y/x */ + auto rr = detail::su_ratio(y.unit(), x.unit()); + + if (rr.natural_unit_.is_dimensionless()) { + r_repr_type r_scale = (static_cast(x.scale()) + - (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(y.scale()))); + + return quantity(r_scale); + } else { + /* units don't match! */ + return quantity(std::numeric_limits::quiet_NaN()); + } + } + }; + } /*namespace detail*/ + + template + requires(quantity_concept + && Q1::always_constexpr_unit) + constexpr auto + with_units(const Q1 & x) { + return x.template rescale_ext(); + } + + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + constexpr auto + with_units_from(const Q1 & x, const Q2 & y) + { + return x.template rescale_ext(); + } + + template + requires (quantity_concept + && Q1::always_constexpr_unit) + constexpr auto + with_repr(const Q1 & x) + { + return x.template with_repr(); + } + + /** @addtogroup quantity-operators **/ + ///@{ + + /** note: won't have constexpr result w/ fractional dimension until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + constexpr auto + operator* (const Q1 & x, const Q2 & y) + { + return detail::quantity_util::multiply(x, y); + } + + /** note: does not require unit scaling, so constexpr with c++23 **/ + template + requires std::is_arithmetic_v && quantity_concept + constexpr auto + operator* (const Quantity & x, Dimensionless y) + { + return x.scale_by(y); + } + + /** note: does not require unit scaling, so constexpr with c++23 **/ + template + requires std::is_arithmetic_v && quantity_concept + constexpr auto + operator* (Dimensionless x, const Quantity & y) + { + return y.scale_by(x); + } + + /** divide quantity @p x by quantity @p y. + * + * note: won't have constexpr result w/ fractional dimension until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + constexpr auto + operator/ (const Q1 & x, const Q2 & y) + { + return detail::quantity_util::divide(x, y); + } + + /** divide quantity @p x by dimensionless value @p y **/ + template + requires std::is_arithmetic_v && quantity_concept + constexpr auto + operator/ (const Quantity & x, Dimensionless y) + { + return x.divide_by(y); + } + + /** divide dimensionless value @p x by quantity @p y **/ + template + requires std::is_arithmetic_v && quantity_concept + constexpr auto + operator/ (Dimensionless x, const Quantity & y) + { + return y.divide_into(x); + } + + /** add quantity @p y to quantity @p x. Result will have the same units as @p x. + * Representation will be the widest of {@c x::repr_type, @c y::repr_type}. + * + * note: won't have constexpr result w/ fractional dimension until c++26 (when @c sqrt(), @c pow() are constexpr) + * + * @pre @p x and @p y expected to have consistent dimensions + **/ + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + constexpr auto + operator+ (const Q1 & x, const Q2 & y) + { + return detail::quantity_util::add(x, y); + } + + /** subtract an arithmetic value from a dimensionless quantity **/ + template + requires (quantity_concept + && Quantity::is_dimensionless() + && std::is_arithmetic_v) + constexpr auto + operator+ (const Quantity & x, Dimensionless y) + { + using repr_type = std::common_type_t; + + auto xp = static_cast(x.scale()); + auto yp = static_cast(y); + + return xp + yp; + } + + /** subtract a dimensionless quantity from an arithmetic value **/ + template + requires (std::is_arithmetic_v + && quantity_concept + && Quantity::is_dimensionless()) + constexpr auto + operator+ (Dimensionless x, const Quantity & y) + { + using repr_type = std::common_type_t; + + auto xp = static_cast(x); + auto yp = static_cast(y.scale()); + + return xp + yp; + } + + /** subtract quantity @p y from quantity @p x. Result will have the same units as @p x. + * Representation will be the widest of {@c x::repr_type, @c y::repr_type} + * + * note: won't have constexpr result w/ fractional dimension until c++26 (when @c sqrt(), @c pow() are constexpr) + * + * @pre @p x and @p y expected to have consistent dimensions + **/ + template + requires (quantity_concept + && quantity_concept + && Q1::always_constexpr_unit + && Q2::always_constexpr_unit) + constexpr auto + operator- (const Q1 & x, const Q2 & y) + { + return detail::quantity_util::subtract(x, y); + } + + /** subtract an arithmetic value from a dimensionless quantity **/ + template + requires (quantity_concept + && Quantity::is_dimensionless() + && std::is_arithmetic_v) + constexpr auto + operator- (const Quantity & x, Dimensionless y) + { + using repr_type = std::common_type_t; + + auto xp = static_cast(x.scale()); + auto yp = static_cast(y); + + return xp - yp; + } + + /** subtract a dimensionless quantity from an arithmetic value **/ + template + requires (std::is_arithmetic_v + && quantity_concept + && Quantity::is_dimensionless()) + constexpr auto + operator- (Dimensionless x, const Quantity & y) + { + using repr_type = std::common_type_t; + + auto xp = static_cast(x); + auto yp = static_cast(y.scale()); + + return xp - yp; + } + + ///@} + + namespace qty { + // ----- mass ----- + + /** create quantity representing @p x picograms of mass, with compile-time unit representation **/ + template + inline constexpr auto picograms(Repr x) { return quantity(x); } + + /** create quantity representing @p x nanograms of mass, with compile-time unit representation **/ + template + inline constexpr auto nanograms(Repr x) { return quantity(x); } + + /** create quantity representing @p x micrograms of mass, with compile-time unit representation **/ + template + inline constexpr auto micrograms(Repr x) { return quantity(x); } + + /** create quantity representing @p x milligrams of mass, with compile-time unit representation **/ + template + inline constexpr auto milligrams(Repr x) { return quantity(x); } + + /** create quantity representing @p x grams of mass, with compile-time unit representation **/ + template + inline constexpr auto grams(Repr x) { return quantity(x); } + + /** create quantity representing @p x kilograms of mass, with compile-time unit representation **/ + template + inline constexpr auto kilograms(Repr x) { return quantity(x); } + + /** create quantity representing @p x tonnes of mass, with compile-time unit representation **/ + template + inline constexpr auto tonnes(Repr x) { return quantity(x); } + + /** create quantity representing @p x kilotonnes of mass, with compile-time unit representation **/ + template + inline constexpr auto kilotonnes(Repr x) { return quantity(x); } + + /** create quantity representing @p x megatonnes of mass, with compile-time unit representation **/ + template + inline constexpr auto megatonnes(Repr x) { return quantity(x); } + + /** create quantity representing @p x gigatonnes of mass, with compile-time unit representation **/ + template + inline constexpr auto gigatonnes(Repr x) { return quantity(x); } + } + + namespace qty { + // ----- mass constants ---- + + /** a quantity representing 1 picogram of mass, with compile-time unit representation **/ + static constexpr auto picogram = picograms(1); + /** a quantity representing 1 nanogram of mass, with compile-time unit representation **/ + static constexpr auto nanogram = nanograms(1); + /** a quantity representing 1 microgram of mass, with compile-time unit representation **/ + static constexpr auto microgram = micrograms(1); + /** a quantity representing 1 milligram of mass, with compile-time unit representation **/ + static constexpr auto milligram = milligrams(1); + /** a quantity representing 1 gram of mass, with compile-time unit representation **/ + static constexpr auto gram = grams(1); + /** a quantity representing 1 kilogram of mass, with compile-time unit representation **/ + static constexpr auto kilogram = kilograms(1); + /** a quantity representing 1 metric tonne of mass, with compile-time unit representation **/ + static constexpr auto tonne = tonnes(1); + /** a quantity representing 1 metric kilotonne of mass, with compile-time unit representation **/ + static constexpr auto kilotonne = kilotonnes(1); + /** a quantity representing 1 metric megatonne of mass, with compile-time unit representation **/ + static constexpr auto megatonne = megatonnes(1); + /** a quantity representing 1 metric gigatonne of mass, with compile-time unit representation **/ + static constexpr auto gigatonne = gigatonnes(1); + } /*namespace qty*/ + + namespace qty { + // ----- distance ----- + + /** create quantity representing @p x picometers of distance, with compile-time unit operations **/ + template + inline constexpr auto picometers(Repr x) { return quantity(x); } + + /** create quantity representing @p x nanometers of distance, with compile-time unit operations **/ + template + inline constexpr auto nanometers(Repr x) { return quantity(x); } + + /** create quantity representing @p x micrometers of distance, with compile-time unit operations **/ + template + inline constexpr auto micrometers(Repr x) { return quantity(x); } + + /** create quantity representing @p x millimeters of distance, with compile-time unit operations **/ + template + inline constexpr auto millimeters(Repr x) { return quantity(x); } + + /** create quantity representing @p x meters of distance, with compile-time unit operations **/ + template + inline constexpr auto meters(Repr x) { return quantity(x); } + + /** create quantity representing @p x kilometers of distance, with compile-time unit operations **/ + template + inline constexpr auto kilometers(Repr x) { return quantity(x); } + + /** create quantity representing @p x megameters of distance, with compile-time unit operations **/ + template + inline constexpr auto megameters(Repr x) { return quantity(x); } + + /** create quantity representing @p x gigameters of distance, + * with compile-time unit operations + **/ + template + inline constexpr auto gigameters(Repr x) { return quantity(x); } + + /** create quantity representing @p x light-seconds of distance, + * with compile-time unit operations. + **/ + template + inline constexpr auto lightseconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x astronomical units of distance, + * with compile-time unit representation + **/ + template + inline constexpr auto astronomicalunits(Repr x) { return quantity(x); } + + /** create quantity representing @p x inches of distance, with compile-time unit representation **/ + template + inline constexpr auto inches(Repr x) { return quantity(x); } + /** create quantity representing @p x feet of distance, with compile-time unit representation **/ + template + inline constexpr auto feet(Repr x) { return quantity(x); } + /** create quantity representing @p x yards of distance, with compile-time unit representation **/ + template + inline constexpr auto yards(Repr x) { return quantity(x); } + /** create quantity representing @p x statute miles of distance, with compile-time unit representation **/ + template + inline constexpr auto miles(Repr x) { return quantity(x); } + } + + namespace qty { + // ----- distance constants ----- + + /** a quantity representing 1 picometer of distance, with compile-time unit representation **/ + static constexpr auto picometer = picometers(1); + /** a quantity representing 1 nanometer of distance, with compile-time unit representation **/ + static constexpr auto nanometer = nanometers(1); + /** a quantity representing 1 micrometer of distance, with compile-time unit representation **/ + static constexpr auto micrometer = micrometers(1); + /** a quantity representing 1 millimeter of distance, with compile-time unit representation **/ + static constexpr auto millimeter = millimeters(1); + /** a quantity representing 1 meter of distance, with compile-time unit representation **/ + static constexpr auto meter = meters(1); + /** a quantity representing 1 kilometer of distance, with compile-time unit representation **/ + static constexpr auto kilometer = kilometers(1); + /** a quantity representing 1 megameter of distance, with compile-time unit representation **/ + static constexpr auto megameter = megameters(1); + /** a quantity representing 1 gigameter of distance, with compile-time unit representation **/ + static constexpr auto gigameter = gigameters(1); + + /** a quantity representing exactly 1 lightsecond of distance, + * with compile-time unit representation + **/ + static constexpr auto lightsecond = lightseconds(1); + /** a quantity representing exactly 1 astronomical unit of distance, + * with compile-time unit representation + **/ + static constexpr auto astronomicalunit = astronomicalunits(1); + + /** a quantity representing 1 inch of distance, with compile-time unit operations **/ + static constexpr auto inch = inches(1); + + /** a quantity representing 1 foot of distance, with compile-time unit operations **/ + static constexpr auto foot = feet(1); + + /** a quantity representing 1 yard of distance, with compile-time unit operations **/ + static constexpr auto yard = yards(1); + + /** a quantity representing 1 mile of distance, with compile-time unit operations **/ + static constexpr auto mile = miles(1); + } /*namespace qty*/ + + namespace qty { + // ----- time ----- + + /** create quantity representing @p x picoseconds of time, with compile-time unit operations **/ + template + inline constexpr auto picoseconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x nanoseconds of time, with compile-time unit operations **/ + template + inline constexpr auto nanoseconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x microseconds of time, with compile-time unit operations **/ + template + inline constexpr auto microseconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x milliseconds of time, with compile-time unit operations **/ + template + inline constexpr auto milliseconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x seconds of time, with compile-time unit operations **/ + template + inline constexpr auto seconds(Repr x) { return quantity(x); } + + /** create quantity representing @p x minutes of time, with compile-time unit operations **/ + template + inline constexpr auto minutes(Repr x) { return quantity(x); } + + /** create quantity representing @p x hours of time, with compile-time unit operations **/ + template + inline constexpr auto hours(Repr x) { return quantity(x); } + + /** create quantity representing @p x exactly-24-hour days of time, with compile-time unit operations **/ + template + inline constexpr auto days(Repr x) { return quantity(x); } + + /** creeate quantity representing @p x weeks of time, + * with compile-time unit operations. Each week has exactly 7 24-hour days. + **/ + template + inline constexpr auto weeks(Repr x) { return quantity(x); } + + /** create quantity representing @p x months of time, + * with compile-time unit operations. Each month has exactly 30 24-hour days + **/ + template + inline constexpr auto months(Repr x) { return quantity(x); } + + /** create quantity representing @p x years of time, + * with compile-time unit operations. Each year has exactly 365.25 24-hour days + **/ + template + inline constexpr auto years(Repr x) { return quantity(x); } + + /** create quantity representing @p x '250-day years' of time. + * 250 represents approximate number of business days in a calendar year. + **/ + template + inline constexpr auto year250s(Repr x) { return quantity(x); } + + /** create quantity representing @p x '360-day years' of time **/ + template + inline constexpr auto year360s(Repr x) { return quantity(x); } + + /** create quantity representing @p x '365-day years' of time **/ + template + inline constexpr auto year365s(Repr x) { return quantity(x); } + } + + namespace qty { + // ----- time constants ---- + + /** a quantity representing 1 picosecond of time, with compile-time unit representation **/ + static constexpr auto picosecond = picoseconds(1); + + /** a quantity representing 1 nanosecond of time, with compile-time unit representation **/ + static constexpr auto nanosecond = nanoseconds(1); + + /** a quantity representing 1 microsecond of time, with compile-time unit representation **/ + static constexpr auto microsecond = microseconds(1); + + /** a quantity representing 1 millisecond of time, with compile-time unit representation **/ + static constexpr auto millisecond = milliseconds(1); + + /** a quantity representing 1 second of time, with compile-time unit representation **/ + static constexpr auto second = seconds(1); + + /** a quantity representing 1 minute of time, with compile-time unit representation **/ + static constexpr auto minute = minutes(1); + + /** a quantity representing 1 hour of time, with compile-time unit representation **/ + static constexpr auto hour = hours(1); + + /** a quantity representing 1 day of time (exactly 24 hours), with compile-time unit representation **/ + static constexpr auto day = days(1); + + /** a quantity representing 1 week of time (7 24-hour days), with compile-time unit representation **/ + static constexpr auto week = weeks(1); + + /** a quantity representing 1 month of time (30 24-hour days), with compile-time unit representation **/ + static constexpr auto month = months(1); + + /** a quantity representing 1 year of time (365.25 24-hour days), with compile-time unit representation **/ + static constexpr auto year = years(1); + + /** a quantity representing 1 250-day year of time, with compile-time unit representation **/ + static constexpr auto year250 = year250s(1); + + /** a quantity representing 1 360-day year of time, with compile-time unit representation **/ + static constexpr auto year360 = year360s(1); + + /** a quantity representing 1 365-day year of time, with compile-time unit representation **/ + static constexpr auto year365 = year365s(1); + + } /*namespace qty*/ + + namespace qty { + // ----- currency ----- + + /** create quantity representing @p x units of currency, with compile-time unit representation **/ + template + inline constexpr auto currency(Repr x) { return quantity(x); } + } + + namespace qty { + // ----- volatility ----- + + /* variance expressed has dimension 1/t; + * volatility ~ sqrt(variance), has dimension 1/sqrt(t) + */ + + /** create quantity representing @p x units of 30-day volatility, with compile-time unit representation **/ + template + inline constexpr auto volatility_30d(Repr x) { return quantity(x); } + + /** create quantity representing @p x units of 250-day volatility, with compile-time unit representation **/ + template + inline constexpr auto volatility_250d(Repr x) { return quantity(x); } + + /** create quantity representing @p x units of 360-day volatility, with compile-time unit representation **/ + template + inline constexpr auto volatility_360d(Repr x) { return quantity(x); } + + /** create quantity representing @p x units of 365-day volatility, with compile-time unit representation **/ + template + inline constexpr auto volatility_365d(Repr x) { return quantity(x); } + } /*namespace qty*/ + + /* reminder: see [quantity_ops.hpp] for operator* etc */ + } /*namespace qty*/ +} /*namespace xo*/ + +/** end quantity.hpp **/ diff --git a/xo-unit/include/xo/unit/quantity_concept.hpp b/xo-unit/include/xo/unit/quantity_concept.hpp new file mode 100644 index 00000000..ec648fb1 --- /dev/null +++ b/xo-unit/include/xo/unit/quantity_concept.hpp @@ -0,0 +1,28 @@ +/** @file quantity_concept.hpp **/ + +#pragma once + +//#include "unit_concept.hpp" +#include "numeric_concept.hpp" + +namespace xo { + namespace qty { + template + concept quantity_concept = requires(Quantity qty, typename Quantity::repr_type repr) + { + typename Quantity::unit_type; + typename Quantity::repr_type; + + //{ Quantity::multiply(qty, qty) }; + + { qty.scale() } -> std::same_as; + { qty.unit() } -> std::same_as; + //{ Quantity::unit_cstr() } -> std::same_as; + //{ Quantity::unit_quantity() } -> std::same_as; + //{ Quantity::promote(repr) } -> std::same_as; + } && (true //unit_concept + && numeric_concept); + } /*namespace qty*/ +} /*namespace xo*/ + +/* end quantity_concept.hpp */ diff --git a/xo-unit/include/xo/unit/quantity_iostream.hpp b/xo-unit/include/xo/unit/quantity_iostream.hpp new file mode 100644 index 00000000..4a1faf73 --- /dev/null +++ b/xo-unit/include/xo/unit/quantity_iostream.hpp @@ -0,0 +1,26 @@ +/** @file quantity_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "quantity.hpp" +#include "natural_unit_iostream.hpp" + +namespace xo { + namespace qty { + template < auto NaturalUnit, typename Repr > + inline std::ostream & + operator<< (std::ostream & os, + const quantity & x) + { + os << x.scale() << x.abbrev(); + return os; + } + + } /*namespace qty*/ + +} /*namespace xo*/ + +/** end quantity_iostream.hpp **/ diff --git a/xo-unit/include/xo/unit/quantity_ops.hpp b/xo-unit/include/xo/unit/quantity_ops.hpp new file mode 100644 index 00000000..33c0c6c3 --- /dev/null +++ b/xo-unit/include/xo/unit/quantity_ops.hpp @@ -0,0 +1,37 @@ +/** @file quantity_ops.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "quantity_concept.hpp" + +namespace xo { + namespace qty { + /** note: won't have constexpr result until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + requires quantity_concept && quantity_concept + constexpr auto + operator== (const Quantity & x, const Quantity2 & y) + { + return (Quantity::compare(x, y) == 0); + } + + /** note: won't have constexpr result until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + requires quantity_concept && quantity_concept + constexpr auto + operator<=> (const Quantity & x, const Quantity2 & y) + { + return Quantity::compare(x, y); + } + + } /*namespace qty*/ + +} /*namespace xo*/ + + +/** end quantity_ops.hpp **/ diff --git a/xo-unit/include/xo/unit/scaled_unit.hpp b/xo-unit/include/xo/unit/scaled_unit.hpp new file mode 100644 index 00000000..43d1e6b5 --- /dev/null +++ b/xo-unit/include/xo/unit/scaled_unit.hpp @@ -0,0 +1,401 @@ +/** @file scaled_unit.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "width2x.hpp" + +namespace xo { + namespace qty { + /** @class scaled_unit + * @brief Represents the product sqrt(outer_scale_sq) * outer_scale_exact * nat_unit + **/ + template < typename Int, + typename OuterScale = ratio::ratio > + struct scaled_unit { + /** @defgroup scaled-unit-type-traits scaled-unit type traits **/ + ///@{ + + /** type for representing individual basis-unit scalefactors **/ + using ratio_int_type = typename natural_unit::ratio_int_type; + + ///@} + + public: + /** @defgroup scaled-unit-ctors scaled-unit constructors **/ + ///@{ + + /** create scaled unit representing a multiple + * @p outer_scale_factor * @p sqrt(outer_scale_sq) + * of natural unit @p nat_unit + **/ + constexpr scaled_unit(const natural_unit & nat_unit, + OuterScale outer_scale_factor, + double outer_scale_sq) + : natural_unit_{nat_unit}, + outer_scale_factor_{outer_scale_factor}, + outer_scale_sq_{outer_scale_sq} + {} + + ///@} + + /** @defgroup scaled-unit-access-methods scaled-unit access methods **/ + ///@{ + + /** always true for scaled_unit **/ + static constexpr bool is_scaled_unit_type_v = true; + + /** always true for scaled_unit **/ + constexpr bool is_scaled_unit_type() const { return true; } + + /** true iff scaled unit can be faithfully represented by a @ref natural_unit **/ + constexpr bool is_natural() const { + return (outer_scale_factor_ == OuterScale(1) && (outer_scale_sq_ == 1.0)); + } + + /** true if this scaled unit has no dimension **/ + constexpr bool is_dimensionless() const { return natural_unit_.is_dimensionless(); } + + /** get number of distinct native dimensions present. + * e.g. for unit Newton = 1 kg.m.s^-2, n_bpu would be 3, + * with {mass, distance, time} present. + * Note that this value does not count exponents + **/ + constexpr std::size_t n_bpu() const { return natural_unit_.n_bpu(); } + + ///@} + + /** @defgroup scaled-unit-general-methods scaled-unit access methods **/ + ///@{ + + /** return reciprocal of this unit. **/ + constexpr scaled_unit reciprocal() const { + return scaled_unit(natural_unit_.reciprocal(), + 1 / outer_scale_factor_, + 1.0 / outer_scale_sq_); + } + + /** get bpu for dimension @p d. if d isn't present, construct bpu with 0 power **/ + constexpr bpu lookup_dim(dimension d) const { + return natural_unit_.lookup_dim(d); + } + + /** return @p i'th bpu associated with this unit **/ + constexpr bpu & operator[](std::size_t i) { return natural_unit_[i]; } + /** return @p i'th bpu associated with this unit (const version) **/ + constexpr const bpu & operator[](std::size_t i) const { return natural_unit_[i]; } + + ///@} + + public: /* public members so scaled_unit instance can be a non-type template parameter (a structural type) */ + + /** @defgroup scaled-unit-instance-vars **/ + ///@{ + + /** scale factor multiplying @ref natural_unit_ **/ + OuterScale outer_scale_factor_; + + /** squared scale factor multiplying @ref natural_unit_ **/ + double outer_scale_sq_; + + /** natural unit term in this scaled unit **/ + natural_unit natural_unit_; + + ///@} + }; + + // TODO: comparison operators + + namespace detail { + /** promote natural unit to scaled unit (with unit outer scalefactors) **/ + template + constexpr auto su_promote(const natural_unit & bpuv) { + return scaled_unit(bpuv, + ratio::ratio(1, 1), + 1.0); + } + } + + namespace u { + /* values here can be used as template arguments to quantity: + * e.g. + * quantity qty1; + * quantity velocity; + */ + + constexpr auto + su_from_bu(const basis_unit & bu, + const power_ratio_type & power = power_ratio_type(1)) + { + return detail::su_promote(natural_unit::from_bu(bu, power)); + } + + /** @defgroup scaled-unit-dimensionless scaled-unit dimensionless constant **/ + ///@{ + + /** dimensionless unit; equivalent to 1 **/ + constexpr auto dimensionless = detail::su_promote(natural_unit()); + + ///@} + + // ----- mass units ----- + + /** @defgroup scaled-unit-mass scaled-unit mass units **/ + ///@{ + + /** unit of 10^-12 grams **/ + constexpr auto picogram = su_from_bu(detail::bu::picogram); + /** unit of 10^-9 grams **/ + constexpr auto nanogram = su_from_bu(detail::bu::nanogram); + /** unit of 10^-6 grams **/ + constexpr auto microgram = su_from_bu(detail::bu::microgram); + /** unit of 10^-3 grams **/ + constexpr auto milligram = su_from_bu(detail::bu::milligram); + /** unit of 1 gram **/ + constexpr auto gram = su_from_bu(detail::bu::gram); + /** unit of 10^3 grams **/ + constexpr auto kilogram = su_from_bu(detail::bu::kilogram); + /** unit of 1 metric tonne = 10^3 kg **/ + constexpr auto tonne = su_from_bu(detail::bu::tonne); + /** unit of 10^3 tonnes = 10^6 kg **/ + constexpr auto kilotonne = su_from_bu(detail::bu::kilotonne); + /** unit of 10^6 tonnes = 10^9 kg **/ + constexpr auto megatonne = su_from_bu(detail::bu::megatonne); + /** unit of 10^9 tonnes = 10^12 kg **/ + constexpr auto gigatonne = su_from_bu(detail::bu::gigatonne); + + ///@} + + // ----- distance units ----- + + /** @defgroup scaled-unit-distance scaled-unit distance units **/ + ///@{ + + /** unit of 10^-12 meters **/ + constexpr auto picometer = su_from_bu(detail::bu::picometer); + /** unit of 10^-9 meters **/ + constexpr auto nanometer = su_from_bu(detail::bu::nanometer); + /** unit of 10^-6 meters **/ + constexpr auto micrometer = su_from_bu(detail::bu::micrometer); + /** unit of 10^-3 meters **/ + constexpr auto millimeter = su_from_bu(detail::bu::millimeter); + /** unit of 1 meter **/ + constexpr auto meter = su_from_bu(detail::bu::meter); + /** unit of 10^3 meters **/ + constexpr auto kilometer = su_from_bu(detail::bu::kilometer); + /** unit of 10^6 meters (not commonly used) **/ + constexpr auto megameter = su_from_bu(detail::bu::megameter); + /** unit of 10^9 meters (not commonly used) **/ + constexpr auto gigameter = su_from_bu(detail::bu::gigameter); + + /** unit of 1 light-second = distance light travels in a vacuum in 1 second **/ + constexpr auto lightsecond = su_from_bu(detail::bu::lightsecond); + /** unit of 1 astronomical unit, for approximate radius of earth orbit **/ + constexpr auto astronomicalunit = su_from_bu(detail::bu::astronomicalunit); + + /** unit of 1 inch = 1/12 feet **/ + constexpr auto inch = su_from_bu(detail::bu::inch); + /** unit of 1 foot = 0.3048 meters **/ + constexpr auto foot = su_from_bu(detail::bu::foot); + /** unit of 1 yard = 3 feet **/ + constexpr auto yard = su_from_bu(detail::bu::yard); + /** unit of 1 mile = 1760 yards **/ + constexpr auto mile = su_from_bu(detail::bu::mile); + + ///@} + + // ----- time units ----- + + /** @defgroup scaled-unit-time scaled-unit time units **/ + ///@{ + + /** unit of 1 picosecond = 10^-12 seconds **/ + constexpr auto picosecond = su_from_bu(detail::bu::picosecond); + /** unit of 1 nanosecond = 10^-9 seconds **/ + constexpr auto nanosecond = su_from_bu(detail::bu::nanosecond); + /** unit of 1 microseccond = 10^-6 seconds **/ + constexpr auto microsecond = su_from_bu(detail::bu::microsecond); + /** unit of 1 millisecond = 10^-3 seconds **/ + constexpr auto millisecond = su_from_bu(detail::bu::millisecond); + /** unit of 1 second **/ + constexpr auto second = su_from_bu(detail::bu::second); + /** unit of 1 minute **/ + constexpr auto minute = su_from_bu(detail::bu::minute); + /** unit of 1 hour **/ + constexpr auto hour = su_from_bu(detail::bu::hour); + /** unit for a 24-hour day **/ + constexpr auto day = su_from_bu(detail::bu::day); + /** unit for a week comprising exactly 7 24-hour days **/ + constexpr auto week = su_from_bu(detail::bu::week); + /** unit for a 30-day month **/ + constexpr auto month = su_from_bu(detail::bu::month); + /** unit for a year containing exactly 365.25 24-hour days **/ + constexpr auto year = su_from_bu(detail::bu::year); + /** unit for a 'year' containing exactly 250 24-hour days. + * (approximates the number of business days in a year) + **/ + constexpr auto year250 = su_from_bu(detail::bu::year250); + /** unit for a 'year' containing exactly 360 24-hour days **/ + constexpr auto year360 = su_from_bu(detail::bu::year360); + /** unit for a 'year' containing exactly 365 24-hour days **/ + constexpr auto year365 = su_from_bu(detail::bu::year365); + + ///@} + + /** @defgroup scaled-unit-misc scaled-unit miscellaneous units **/ + ///@{ + + // ----- currency ----- + + /** generic currency unit **/ + constexpr auto currency = su_from_bu(detail::bu::currency); + + // ----- price - --- + + /** generic price unit **/ + constexpr auto price = su_from_bu(detail::bu::price); + + ///@} + + // ----- volatility units ----- + + /** @defgroup scaled-unit-volatility scaled-unit volatility units **/ + ///@{ + + /** volatility, in 30-day units **/ + constexpr auto volatility_30d = su_from_bu(detail::bu::month, + power_ratio_type(-1,2)); + /** volatility, in 250-day 'annual' units **/ + constexpr auto volatility_250d = su_from_bu(detail::bu::year250, + power_ratio_type(-1,2)); + /** volatility, in 360-day 'annual' units **/ + constexpr auto volatility_360d = su_from_bu(detail::bu::year360, + power_ratio_type(-1,2)); + /** volatility, in 365-day 'annual' units **/ + constexpr auto volatility_365d = su_from_bu(detail::bu::year365, + power_ratio_type(-1,2)); + ///@} + } + + namespace detail { + template , + typename OuterScale = ratio::ratio> + constexpr + scaled_unit + su_product(const natural_unit & lhs_bpu_array, + const natural_unit & rhs_bpu_array) + { + natural_unit prod = lhs_bpu_array.template to_repr(); + + /* accumulate product of scalefactors spun off by rescaling + * any basis-units in rhs_bpu_array that conflict with the same dimension + * in lh_bpu_array + */ + auto sfr = (detail::outer_scalefactor_result + (OuterScale(1) /*outer_scale_factor*/, + 1.0 /*outer_scale_sq*/)); + + for (std::size_t i = 0; i < rhs_bpu_array.n_bpu(); ++i) { + auto sfr2 = nu_product_inplace(&prod, + rhs_bpu_array[i].template to_repr()); + + sfr.outer_scale_factor_ = sfr.outer_scale_factor_ * sfr2.outer_scale_factor_; + sfr.outer_scale_sq_ *= sfr2.outer_scale_sq_; + } + + return scaled_unit(prod.template to_repr(), + sfr.outer_scale_factor_, + sfr.outer_scale_sq_); + } + + /* use Int2x to accumulate scalefactor + */ + template < typename Int, + typename Int2x = width2x, + typename OuterScale = ratio::ratio > + constexpr + scaled_unit + su_ratio(const natural_unit & nu_lhs, + const natural_unit & nu_rhs) + { + natural_unit ratio = nu_lhs.template to_repr(); + + /* accumulate product of scalefactors spun off by rescaling + * any basis-units in rhs_bpu_array that conflict with the same dimension + * in lh_bpu_array + */ + auto sfr = (detail::outer_scalefactor_result + (OuterScale(1) /*outer_scale_factor*/, + 1.0 /*outer_scale_sq*/)); + + for (std::size_t i = 0; i < nu_rhs.n_bpu(); ++i) { + auto sfr2 = nu_ratio_inplace(&ratio, + nu_rhs[i].template to_repr()); + + /* note: nu_ratio_inplace() reports multiplicative outer scaling factors, + * so multiply is correct here + */ + sfr.outer_scale_factor_ = (sfr.outer_scale_factor_ + * sfr2.outer_scale_factor_); + sfr.outer_scale_sq_ *= sfr2.outer_scale_sq_; + } + + return scaled_unit(ratio.template to_repr(), + sfr.outer_scale_factor_, + sfr.outer_scale_sq_); + } + } + + /** @defgroup scaled-unit-operators **/ + ///@{ + + /** Multiply scaled_unit instances @p x_unit and @p y_unit. + * Result is a scaled_unit for the product dimension. + * For each basis dimension, result will prioritize scale from @p x_unit ahead of @p y_unit. + **/ + template > + inline constexpr scaled_unit + operator* (const scaled_unit & x_unit, + const scaled_unit & y_unit) + { + auto rr = detail::su_product(x_unit.natural_unit_, + y_unit.natural_unit_); + + return (scaled_unit + (rr.natural_unit_, + (ratio::ratio(rr.outer_scale_factor_) + * ratio::ratio(x_unit.outer_scale_factor_) + * ratio::ratio(y_unit.outer_scale_factor_)), + rr.outer_scale_sq_ * x_unit.outer_scale_sq_ * y_unit.outer_scale_sq_)); + } + + /** Divide scaled_unit instances @p x_unit by @p y_unit. + * Result is a scaled_unit for the quotient dimension. + * For each basis dimension, result will prioritize scale from @p x_unit ahead of @p y_unit. + **/ + template > + inline constexpr scaled_unit + operator/ (const scaled_unit & x_unit, + const scaled_unit & y_unit) + { + auto rr = detail::su_ratio(x_unit.natural_unit_, + y_unit.natural_unit_); + + return (scaled_unit + (rr.natural_unit_, + (ratio::ratio(rr.outer_scale_factor_) + * ratio::ratio(x_unit.outer_scale_factor_) + * ratio::ratio(y_unit.outer_scale_factor_)), + rr.outer_scale_sq_ * x_unit.outer_scale_sq_ * y_unit.outer_scale_sq_)); + } + + ///@} + } /*namespace qty*/ +} /*namespace xo*/ + +/** end scaled_unit.hpp **/ diff --git a/xo-unit/include/xo/unit/scaled_unit_concept.hpp b/xo-unit/include/xo/unit/scaled_unit_concept.hpp new file mode 100644 index 00000000..f05a0507 --- /dev/null +++ b/xo-unit/include/xo/unit/scaled_unit_concept.hpp @@ -0,0 +1,23 @@ +/** @file scaled_unit_concept.hpp **/ + +#pragma once + +#include + +namespace xo { + namespace qty { + template + concept scaled_unit_concept = requires(ScaledUnit su) + { + typename ScaledUnit::ratio_int_type; + + { su.is_scaled_unit_type() } -> std::same_as; + { su.is_natural() } -> std::same_as; + { su.is_dimensionless() } -> std::same_as; + + } && ScaledUnit::is_scaled_unit_type_v; + } /*namespace qty*/ +} /*namespace xo*/ + + +/** end scaled_unit_concept.hpp **/ diff --git a/xo-unit/include/xo/unit/scaled_unit_iostream.hpp b/xo-unit/include/xo/unit/scaled_unit_iostream.hpp new file mode 100644 index 00000000..789b39c9 --- /dev/null +++ b/xo-unit/include/xo/unit/scaled_unit_iostream.hpp @@ -0,0 +1,31 @@ +/** @file scaled_unit_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "scaled_unit.hpp" +#include "natural_unit_iostream.hpp" +#include "xo/flatstring/int128_iostream.hpp" +#include + +namespace xo { + namespace qty { + template + inline std::ostream & + operator<<(std::ostream & os, + const scaled_unit & x) + { + os << ""; + + return os; + }; + } /*namespace qty*/ +} /*namespace xo*/ + +/** end scaled_unit_iostream.hpp **/ diff --git a/xo-unit/include/xo/unit/unit2.hpp b/xo-unit/include/xo/unit/unit2.hpp new file mode 100644 index 00000000..eab1bf8f --- /dev/null +++ b/xo-unit/include/xo/unit/unit2.hpp @@ -0,0 +1,24 @@ +/** @file unit2.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "natural_unit.hpp" + +namespace xo { + namespace qty { + /** @class unit2 + * @brief represent an arbitrary unit along with dimension details + * + * For example, + * kg.m.s^-2 or + * (kilogram * meter) / (second * second) + **/ + template + using unit2 = natural_unit; + } /*namespace qty*/ +} /*namespace xo*/ + +/** end unit2.hpp **/ diff --git a/xo-unit/include/xo/unit/width2x.hpp b/xo-unit/include/xo/unit/width2x.hpp new file mode 100644 index 00000000..6047486c --- /dev/null +++ b/xo-unit/include/xo/unit/width2x.hpp @@ -0,0 +1,39 @@ +/** @file width2x.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "natural_unit.hpp" +#include + +namespace xo { + namespace qty { + namespace detail { + template + struct width2x; + + template <> + struct width2x { + using type = std::int32_t; + }; + + template <> + struct width2x { + using type = std::int64_t; + }; + + template <> + struct width2x { + using type = __int128_t; + }; + + template + using width2x_t = width2x::type; + } + + } /*namespace qty*/ +} /*namespace xo*/ + +/** end width2x.hpp **/ diff --git a/xo-unit/include/xo/unit/xquantity.hpp b/xo-unit/include/xo/unit/xquantity.hpp new file mode 100644 index 00000000..08b7e29c --- /dev/null +++ b/xo-unit/include/xo/unit/xquantity.hpp @@ -0,0 +1,562 @@ +/** @file xquantity.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "quantity_ops.hpp" +#include "scaled_unit.hpp" +#include "natural_unit.hpp" + +namespace xo { + namespace qty { + /** @class xquantity + * @brief represent a scalar quantity with polymorphic units. + * + * - @p Repr type used represent a dimensionless multiple of a natural unit. + * + * Constexpr implementation, but units are explicitly represented: + * @code + * sizeof(xquantity) > sizeof(xquantity::repr_type) + * @endcode + * + * Explicit unit representation allows introducing units at runtime, + * for example in python bindings. + * See for example xo-pyutil + * + * See @ref quantity for implementation with units established at compile time + * + * Require: + * - Repr supports numeric operations (+, -, *, /) + * - Repr supports conversion from double. + **/ + template + class xquantity { + public: + /** @defgroup xquantity-type-traits xquantity type traits **/ + ///@{ + /** @brief runtime representation for this value's scale **/ + using repr_type = Repr; + /** @brief runtime representation for this value's unit **/ + using unit_type = natural_unit; + /** @brief type used for numerator and denominator in basis-unit scalefactor ratios **/ + using ratio_int_type = Int; + /** @brief double-width type for numerator and denominator of intermediate + * scalefactor ratios. Use to mitigate loss of precision during computation + * of conversion factors between units with widely-differing magnitude + **/ + using ratio_int2x_type = detail::width2x_t; + ///@} + + public: + /** @defgroup xquantity-ctors xquantity constructors **/ + ///@{ + + /** create dimensionless, zero quantity **/ + constexpr xquantity() + : scale_{0}, unit_{natural_unit()} {} + /** create quantity representing multiple of @p scale times @p unit **/ + constexpr xquantity(Repr scale, + const natural_unit & unit) + : scale_{scale}, unit_{unit} {} + /** create quantity representing multiple of @p scale times @p unit. + * + * Collects outer scalefactors (if any) from @p unit, + * so for example: + * + * @code + * using namespace xo::qty; + * xquantity q(123, u::meter * u::millimeter); + * + * q.scale() --> 0.123 + * @endcode + **/ + constexpr xquantity(Repr scale, + const scaled_unit & unit) + : + scale_(scale + * unit.outer_scale_factor_.template convert_to() + * ((unit.outer_scale_sq_ == 1.0) + ? 1.0 + : ::sqrt(unit.outer_scale_sq_)) + ), + unit_{unit.natural_unit_} {} + + ///@} + + /** @defgroup xquantity-constants static xquantity constants **/ + ///@{ + + /** false since unit information may be unknown at compile time. + * Coordinates with @c quantity::always_constexpr_unit + **/ + static constexpr bool always_constexpr_unit = false; + + ///@} + + /** @defgroup xquantity-access-methods xquantity access methods **/ + ///@{ + + /** get member @ref scale_ **/ + constexpr const repr_type & scale() const { return scale_; } + /** get member @ref unit_ **/ + constexpr const unit_type & unit() const { return unit_; } + + /** true iff this quantity has no dimension **/ + constexpr bool is_dimensionless() const { return unit_.is_dimensionless(); } + + /** return abbreviation for quantities with this unit **/ + constexpr nu_abbrev_type abbrev() const { return unit_.abbrev(); } + + ///@} + + /** @defgroup xquantity-arithmetic-support**/ + ///@{ + + /** create unit quantity with same unit as @c this **/ + constexpr xquantity unit_qty() const { return xquantity(1, unit_); } + + /** create zero quantity with same unit as @c this **/ + constexpr xquantity zero_qty() const { return xquantity(0, unit_); } + + /** create quantity representing reciprocal of @c this **/ + constexpr xquantity reciprocal() const { return xquantity(1.0 / scale_, unit_.reciprocal()); } + + /** create quantity representing this value scaled by dimensionless mutliplier @p x **/ + template + requires std::is_arithmetic_v + constexpr auto scale_by(Dimensionless x) const { + return xquantity(x * this->scale_, this->unit_); + } + + /** create quantity representing this value scaled by dimensionless multiplier @p 1/x **/ + template + requires std::is_arithmetic_v + constexpr auto divide_by(Dimensionless x) const { + return xquantity(this->scale_ / x, this->unit_); + } + + /** create quantity representing dimensionless numerator @p x divided by this value **/ + template + requires std::is_arithmetic_v + constexpr auto divide_into(Dimensionless x) const { + return xquantity(x / this->scale_, this->unit_.reciprocal()); + } + + /** multiply two @c xquantity values, or a mixed (@c xquantity, @c quantity) pair **/ + template + static constexpr + auto multiply(const xquantity & x, const Quantity2 & y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + auto rr = detail::su_product(x.unit(), y.unit()); + + r_repr_type r_scale = (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(x.scale()) + * static_cast(y.scale())); + + return xquantity(r_scale, + rr.natural_unit_); + } + + /** compute quotient @p x / @p y, where @p x and @p y are xquantities **/ + template + static constexpr + auto divide(const xquantity & x, const Quantity2 & y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + auto rr = detail::su_ratio(x.unit(), y.unit()); + + /* note: su_ratio() reports multiplicative outer scaling factors, + * so multiply is correct here + */ + r_repr_type r_scale = (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(x.scale()) + / static_cast(y.scale())); + + return xquantity(r_scale, + rr.natural_unit_); + } + + /** compute sum @p x + @p y, where @p x and @p y are xquantities **/ + template + static constexpr + auto add(const xquantity & x, const Quantity2 & y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + /* conversion to get y in same units as x: multiply by y/x */ + auto rr = detail::su_ratio(y.unit(), x.unit()); + + if (rr.natural_unit_.is_dimensionless()) { + r_repr_type r_scale = (static_cast(x.scale()) + + (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(y.scale()))); + + return xquantity(r_scale, x.unit_.template to_repr()); + } else { + /* units don't match! */ + return xquantity(std::numeric_limits::quiet_NaN(), + x.unit_.template to_repr()); + } + } + + /** compute difference @p x - @p y, where @p x and @p y are xquantities **/ + template + static constexpr + auto subtract(const xquantity & x, const Quantity2 & y) { + using r_repr_type = std::common_type_t; + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + /* conversion to get y in same units as x: multiply by y/x */ + auto rr = detail::su_ratio(y.unit(), x.unit()); + + if (rr.natural_unit_.is_dimensionless()) { + r_repr_type r_scale = (static_cast(x.scale()) + - (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * static_cast(y.scale()))); + + return xquantity(r_scale, x.unit_.template to_repr()); + } else { + /* units don't match! */ + return xquantity(std::numeric_limits::quiet_NaN(), + x.unit_.template to_repr()); + } + } + + ///@} + + /** @defgroup xquantity-unit-conversion **/ + ///@{ + + /** create quantity representing the same value, but in units of @p unit2 **/ + constexpr + auto rescale(const natural_unit & unit2) const { + /* conversion factor from .unit -> unit2*/ + auto rr = detail::su_ratio(this->unit_, unit2); + + if (rr.natural_unit_.is_dimensionless()) { + repr_type r_scale = (::sqrt(rr.outer_scale_sq_) + * rr.outer_scale_factor_.template convert_to() + * this->scale_); + return xquantity(r_scale, unit2); + } else { + return xquantity(std::numeric_limits::quiet_NaN(), unit2); + } + } + + constexpr + auto rescale_ext(const scaled_unit & unit2) const { + /* conversion factor from .unit -> unit2*/ + auto rr = detail::su_ratio(unit_, + unit2.natural_unit_); + + if (rr.natural_unit_.is_dimensionless()) { + /* NOTE: test for unit .outer_scale_sq values to get constexpr result with c++23 + * and integer dimension powers. + * + * NOTE: we don't intend to support mixed-unit quantities. + * If we change intention, will need to take into account + * (s_scaled_unit.outer_scale_factor_, s_scaled_unit.outer_scale_sq_) + */ + repr_type r_scale + = ((((rr.outer_scale_sq_ == 1.0) + && (unit2.outer_scale_sq_ == 1.0)) + ? 1.0 + : ::sqrt(rr.outer_scale_sq_ / unit2.outer_scale_sq_)) + * rr.outer_scale_factor_.template convert_to() + * this->scale_ + / unit2.outer_scale_factor_.template convert_to()); + return xquantity(r_scale, unit2); + } else { + return xquantity(std::numeric_limits::quiet_NaN(), unit2); + } + } + + ///@} + + /** @defgroup xquantity-comparison-support xquantity comparison support methods **/ + ///@{ + + /** perform 3-way comparison between @c xquantity values @p x and @p y **/ + template + static constexpr + auto compare(const xquantity & x, const Quantity2 & y) { + xquantity y2 = y.rescale(x.unit_); + + return x.scale() <=> y2.scale(); + } + + ///@} + + /** @defgroup xquantity-operators xquantity operators **/ + ///@{ + + /** add @p x in-place, converting units if necessary **/ + template + xquantity & operator+= (const Quantity2 & x) { + *this = *this + x; + return *this; + } + + /** unary negation; preserves unit information **/ + xquantity operator-() const { + return xquantity(-scale_, unit_); + } + + /** subtract @p x in-place, converting units if necessary **/ + template + xquantity & operator-= (const Quantity2 & x) { + *this = *this - x; + return *this; + } + + /** multiply @p x in-place, converting units if necessary + * + * @note: unlike @c quantity::operator*=, may change dimension of lhs + **/ + template + xquantity & operator*= (const Quantity2 & x) { + *this = *this * x; + return *this; + } + + /** divide @p x in-place, converting units if necessary + * + * @note: unlike @c quantity::operator/=, may change dimension of lhs + **/ + template + xquantity & operator/= (const Quantity2 & x) { + *this = *this / x; + return *this; + } + + ///@} + + private: + /** @defgroup xquantity-instance-vars **/ + ///@{ + + /** quantity represents this multiple of a unit amount **/ + Repr scale_ = Repr{}; + /** unit for this quantity **/ + natural_unit unit_; + + ///@} + }; /*xquantity*/ + + /** note: won't have constexpr result until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + inline constexpr xquantity + unit_qty(const scaled_unit & u) + { + return xquantity + (u.outer_scale_factor_.template convert_to() * ::sqrt(u.outer_scale_sq_), + u.natural_unit_); + } + + /** note: won't have constexpr result until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + inline constexpr xquantity + natural_unit_qty(const natural_unit & nu) { + return xquantity(1.0, nu); + } + + /** note: won't have constexpr result until c++26 (when @c sqrt(), @c pow() are constexpr) + **/ + template + requires (quantity_concept + && quantity_concept + && (!Q1::always_constexpr_unit || !Q2::always_constexpr_unit)) + constexpr auto + operator* (const Q1 & x, const Q2 & y) + { + return Q1::multiply(x, y); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator* (double x, const Quantity & y) + { + return y.scale_by(x); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator* (const Quantity & x, double y) + { + return x.scale_by(y); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept && quantity_concept + constexpr auto + operator/ (const Quantity & x, const Quantity2 & y) + { + return Quantity::divide(x, y); + } + + /** note: doesn not require unit scaling, so constexpr with c++23 **/ + template + requires quantity_concept && std::is_arithmetic_v + constexpr auto + operator/ (const Quantity & x, Dimensionless y) + { + return x.divide_by(y); + } + + /** note: doesn not require unit scaling, so constexpr with c++23 **/ + template + requires std::is_arithmetic_v && quantity_concept + constexpr auto + operator/ (Dimensionless x, const Quantity & y) + { + return y.divide_into(x); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept && quantity_concept + constexpr auto + operator+ (const Quantity & x, const Quantity2 & y) + { + return Quantity::add(x, y); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator+ (const Quantity & x, double y) + { + return x + Quantity(y, u::dimensionless); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator+ (double x, const Quantity & y) + { + return Quantity(x, u::dimensionless) + y; + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires (quantity_concept + && quantity_concept) + constexpr auto + operator- (const Quantity & x, const Quantity2 & y) + { + return Quantity::subtract(x, y); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator- (const Quantity & x, double y) + { + return x - Quantity(y, u::dimensionless); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator- (double x, const Quantity & y) + { + return Quantity(x, u::dimensionless) - y; + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator== (const Quantity & x, double y) + { + return (x == Quantity(y, u::dimensionless)); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator== (double x, const Quantity & y) + { + return (Quantity(x, u::dimensionless) == y); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator<=> (const Quantity & x, double y) + { + return Quantity::compare(x, Quantity(y, u::dimensionless)); + } + + /** note: won't have constexpr result until c++26 (when ::sqrt(), ::pow() are constexpr) + **/ + template + requires quantity_concept + constexpr auto + operator<=> (double x, const Quantity & y) + { + return Quantity::compare(Quantity(x, u::dimensionless), y); + } + + namespace xu { + constexpr auto nanogram = xquantity(1.0, u::nanogram); + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end xquantity.hpp **/ diff --git a/xo-unit/include/xo/unit/xquantity_iostream.hpp b/xo-unit/include/xo/unit/xquantity_iostream.hpp new file mode 100644 index 00000000..f428027e --- /dev/null +++ b/xo-unit/include/xo/unit/xquantity_iostream.hpp @@ -0,0 +1,27 @@ +/** @file xquantity_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "xquantity.hpp" +#include "natural_unit_iostream.hpp" +//#include + +namespace xo { + namespace qty { + template + inline std::ostream & + operator<< (std::ostream & os, + const xquantity & x) + { + os << x.scale() << x.abbrev(); + + return os; + } + } /*namespace qty*/ +} /*namespace xo*/ + +/** end xquantity_iostream.hpp **/ diff --git a/xo-unit/pkgs/xo-cmake.nix b/xo-unit/pkgs/xo-cmake.nix new file mode 100644 index 00000000..91a8cbd2 --- /dev/null +++ b/xo-unit/pkgs/xo-cmake.nix @@ -0,0 +1,38 @@ +{ + # dependencies + stdenv, cmake, # ... other deps here + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-cmake"; + +# src = cmake-examples-ex1-path; + + src = (fetchGit { + url = "https://github.com/rconybea/xo-cmake"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + nativeBuildInputs = [ cmake ]; + +# installPhase = '' +# mkdir -p $out +# echo 'This project intentionally has no install phase' +# ''; + }) diff --git a/xo-unit/pkgs/xo-flatstring.nix b/xo-unit/pkgs/xo-flatstring.nix new file mode 100644 index 00000000..dbaf6dbc --- /dev/null +++ b/xo-unit/pkgs/xo-flatstring.nix @@ -0,0 +1,75 @@ +{ + # 1. nixpkgs dependencies + # 1.1. python + python, pythonPackages, + + # 1.2. particular python packages + sphinx ? pythonPackages.sphinx, + sphinx-rtd-theme ? pythonPackages.sphinx-rtd-theme, + breathe ? pythonPackages.breathe, + + # 1.3. document-generation packages + # use of makeFontsConf adapted from nixpkgs/development/libraries/gtkmm/4.x.nix + # + doxygen, graphviz, + ##fontconfig, + ##makeFontsConf, + + # 1.4. c++ build/utest chain + stdenv, cmake, catch2, # ... other deps here + + # 2. xo dependencies + xo-cmake, xo-indentlog + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-flatstring"; + + src = (fetchGit { + url = "https://github.com/rconybea/xo-flatstring"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + + # move HOME so fontconfig can do sensible things + buildPhase = '' + #set -x + + echo "FONTCONFIG_FILE=$FONTCONFIG_FILE" + + #export FONTCONFIG_FILE=$fontconfig.out/etc/fonts/fonts.conf + export HOME=$TMPDIR + export XDG_CONFIG_HOME=$TMPDIR + + mkdir $XDG_CONFIG_HOME/fontconfig + + #grep xdg $FONTCONFIG_FILE + + #$fontconfig.out/bin/fc-cache -v + + make && make docs + #make + ''; + + doCheck = true; + nativeBuildInputs = [ cmake catch2 + doxygen graphviz sphinx sphinx-rtd-theme breathe ]; + propagatedBuildInputs = [ xo-indentlog ]; + }) diff --git a/xo-unit/pkgs/xo-indentlog.nix b/xo-unit/pkgs/xo-indentlog.nix new file mode 100644 index 00000000..fe0a5364 --- /dev/null +++ b/xo-unit/pkgs/xo-indentlog.nix @@ -0,0 +1,36 @@ +{ + # nixpkgs dependencies + stdenv, cmake, catch2, # ... other deps here + + # xo dependencies + xo-cmake, + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-indentlog"; + + src = (fetchGit { + url = "https://github.com/rconybea/indentlog"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + doCheck = true; + nativeBuildInputs = [ cmake catch2 ]; + }) diff --git a/xo-unit/pkgs/xo-randomgen.nix b/xo-unit/pkgs/xo-randomgen.nix new file mode 100644 index 00000000..a46af546 --- /dev/null +++ b/xo-unit/pkgs/xo-randomgen.nix @@ -0,0 +1,37 @@ +{ + # nixpkgs dependencies + stdenv, cmake, catch2, # ... other deps here + + # xo dependencies + xo-cmake, xo-indentlog + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-randomgen"; + + src = (fetchGit { + url = "https://github.com/rconybea/randomgen"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + doCheck = true; + nativeBuildInputs = [ cmake catch2 ]; + propagatedBuildInputs = [ xo-indentlog ]; + }) diff --git a/xo-unit/pkgs/xo-ratio.nix b/xo-unit/pkgs/xo-ratio.nix new file mode 100644 index 00000000..34c01478 --- /dev/null +++ b/xo-unit/pkgs/xo-ratio.nix @@ -0,0 +1,83 @@ +{ + # 1. nixpkgs dependencies + # 1.1. python + python, pythonPackages, + + # 1.2. particular python packages + sphinx ? pythonPackages.sphinx, + sphinx-rtd-theme ? pythonPackages.sphinx-rtd-theme, + breathe ? pythonPackages.breathe, + + # 1.3. document-generation packages + # use of makeFontsConf adapted from nixpkgs/development/libraries/gtkmm/4.x.nix + # + doxygen, graphviz, + ##fontconfig, + ##makeFontsConf, + + # used for graphviz (dot) see docs/Doxyfile.in + #helvetica-neue-lt-std, + + # 1.4. c++ build/utest chain + stdenv, cmake, catch2, # ... other deps here + + # 2. xo dependencies + xo-cmake, xo-flatstring, xo-randomgen, + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-ratio"; + + src = (fetchGit { + url = "https://github.com/rconybea/xo-ratio"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + + # NOTE: helvetic-neue-lt-std has unfree license + #FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ helvetica-neue-lt-std ]; }; + + # move HOME so fontconfig can do sensible things + buildPhase = '' + #set -x + + echo "FONTCONFIG_FILE=$FONTCONFIG_FILE" + + #export FONTCONFIG_FILE=$fontconfig.out/etc/fonts/fonts.conf + export HOME=$TMPDIR + export XDG_CONFIG_HOME=$TMPDIR + + mkdir $XDG_CONFIG_HOME/fontconfig + + #grep xdg $FONTCONFIG_FILE + + #$fontconfig.out/bin/fc-cache -v + + make && make docs + #make + ''; + + doCheck = true; + nativeBuildInputs = [ cmake catch2 + doxygen graphviz sphinx sphinx-rtd-theme breathe + xo-randomgen + ]; + propagatedBuildInputs = [ xo-flatstring ]; + }) diff --git a/xo-unit/pkgs/xo-refcnt.nix b/xo-unit/pkgs/xo-refcnt.nix new file mode 100644 index 00000000..cd47dbe9 --- /dev/null +++ b/xo-unit/pkgs/xo-refcnt.nix @@ -0,0 +1,37 @@ +{ + # nixpkgs dependencies + stdenv, cmake, catch2, # ... other deps here + + # xo dependencies + xo-cmake, xo-indentlog + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-refcnt"; + + src = (fetchGit { + url = "https://github.com/rconybea/refcnt"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + doCheck = true; + nativeBuildInputs = [ cmake catch2 ]; + propagatedBuildInputs = [ xo-indentlog ]; + }) diff --git a/xo-unit/pkgs/xo-reflect.nix b/xo-unit/pkgs/xo-reflect.nix new file mode 100644 index 00000000..9e0ad656 --- /dev/null +++ b/xo-unit/pkgs/xo-reflect.nix @@ -0,0 +1,37 @@ +{ + # nixpkgs dependencies + stdenv, cmake, catch2, # ... other deps here + + # xo dependencies + xo-cmake, xo-refcnt, xo-subsys, + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-reflect"; + + src = (fetchGit { + url = "https://github.com/rconybea/reflect"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + doCheck = true; + nativeBuildInputs = [ cmake catch2 ]; + propagatedBuildInputs = [ xo-subsys xo-refcnt ]; + }) diff --git a/xo-unit/pkgs/xo-subsys.nix b/xo-unit/pkgs/xo-subsys.nix new file mode 100644 index 00000000..79791b20 --- /dev/null +++ b/xo-unit/pkgs/xo-subsys.nix @@ -0,0 +1,36 @@ +{ + # nixpkgs dependencies + stdenv, cmake, catch2, # ... other deps here + + # xo dependencies + xo-cmake, + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-subsys"; + + src = (fetchGit { + url = "https://github.com/rconybea/subsys"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + #doCheck = true; + nativeBuildInputs = [ cmake catch2 ]; + }) diff --git a/xo-unit/pkgs/xo-unit.nix b/xo-unit/pkgs/xo-unit.nix new file mode 100644 index 00000000..b19c11a4 --- /dev/null +++ b/xo-unit/pkgs/xo-unit.nix @@ -0,0 +1,79 @@ +{ + # 1. nixpkgs dependencies + # 1.1. python + python, pythonPackages, + + # 1.2. particular python packages + sphinx ? pythonPackages.sphinx, + sphinx-rtd-theme ? pythonPackages.sphinx-rtd-theme, + breathe ? pythonPackages.breathe, + + # 1.3. document-generation packages + # use of makeFontsConf adapted from nixpkgs/development/libraries/gtkmm/4.x.nix + # + doxygen, graphviz, + #fontconfig, + makeFontsConf, + + # used for graphviz (dot) see docs/Doxyfile.in + #helvetica-neue-lt-std, + + # 1.4. c++ build/utest chain + stdenv, cmake, catch2, # ... other deps here + + # 2. xo dependencies + xo-cmake, xo-flatstring, xo-ratio, xo-randomgen, + + # args + + # attrset for fetching source code. + # { type, owner, repo, ref } + # + # e.g. type="github", owner="rconybea", repo="cmake-examples", ref="ex1b" + # + # see [[../flake.nix]] + # + #cmake-examples-ex1-path + + # someconfigurationoption ? false +} : + +stdenv.mkDerivation (finalattrs: + { + name = "xo-unit"; + + src = (fetchGit { + url = "https://github.com/rconybea/xo-unit"; + version = "1.0"; + #ref = "ex1"; + #rev = "c0472c9d7e4d2c53bfb977d3182380832fe96645"; + }); + + cmakeFlags = ["-DXO_CMAKE_CONFIG_EXECUTABLE=${xo-cmake}/bin/xo-cmake-config"]; + + # NOTE: helvetic-neue-lt-std has unfree license + #FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ helvetica-neue-lt-std ]; }; + + # move HOME so fontconfig can do sensible things + buildPhase = '' + set -x + + echo "FONTCONFIG_FILE=$FONTCONFIG_FILE" + + #export FONTCONFIG_FILE=$fontconfig.out/etc/fonts/fonts.conf + export HOME=$TMPDIR + export XDG_CONFIG_HOME=$TMPDIR + + mkdir $XDG_CONFIG_HOME/fontconfig + + #grep xdg $FONTCONFIG_FILE + + #$fontconfig.out/bin/fc-cache -v + + make && make docs + ''; + + doCheck = true; + nativeBuildInputs = [ cmake catch2 doxygen graphviz sphinx sphinx-rtd-theme breathe xo-randomgen ]; + propagatedBuildInputs = [ xo-flatstring xo-ratio ]; + }) diff --git a/xo-unit/pkgs/xo-userenv.nix b/xo-unit/pkgs/xo-userenv.nix new file mode 100644 index 00000000..8dfce6c7 --- /dev/null +++ b/xo-unit/pkgs/xo-userenv.nix @@ -0,0 +1,27 @@ +{ + # nixpkgs dependencies + buildFHSUserEnv, # ... other deps here + + # xo dependencies + xo-cmake, xo-indentlog, xo-flatstring, xo-subsys, xo-refcnt, xo-ratio, xo-reflect, xo-randomgen, xo-unit, + + # other args + + # someconfigurationoption ? false +} : + +buildFHSUserEnv { + name = "xo-userenv"; + targetPkgs = pkgs: [ xo-cmake + xo-indentlog + xo-flatstring + xo-subsys + xo-refcnt + xo-ratio + xo-reflect + xo-randomgen + xo-unit + ]; + # runScript = ...; + # profile = ...; +} diff --git a/xo-unit/utest/CMakeLists.txt b/xo-unit/utest/CMakeLists.txt new file mode 100644 index 00000000..2ca38d03 --- /dev/null +++ b/xo-unit/utest/CMakeLists.txt @@ -0,0 +1,26 @@ +# xo-unit/utest/CMakeLists.txt + +set(SELF_EXE utest.unit) +set(SELF_SRCS + unit_utest_main.cpp #mpl_unit.test.cpp + xquantity.test.cpp + quantity.test.cpp + bpu.test.cpp + basis_unit.test.cpp + scaled_unit.test.cpp + natural_unit.test.cpp + unit.test.cpp #quantity.test.cpp +) + +if (ENABLE_TESTING) + xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) + + # ---------------------------------------------------------------- + # dependencies.. + + xo_self_dependency(${SELF_EXE} xo_unit) + xo_headeronly_dependency(${SELF_EXE} xo_ratio) + xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2) +endif() + +# end CMakeLists.txt diff --git a/xo-unit/utest/basis_unit.test.cpp b/xo-unit/utest/basis_unit.test.cpp new file mode 100644 index 00000000..0874db56 --- /dev/null +++ b/xo-unit/utest/basis_unit.test.cpp @@ -0,0 +1,153 @@ +/* @file bu.test.cpp */ + +#include "xo/unit/basis_unit.hpp" +#include "xo/unit/bu_store.hpp" +#include "xo/indentlog/scope.hpp" +//#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + using xo::qty::scalefactor_ratio_type; + using xo::qty::bu_abbrev; + using xo::qty::basis_unit; + using xo::qty::bu_abbrev_type; + using xo::qty::native_unit2_v; + using xo::qty::dim; + namespace bu = xo::qty::detail::bu; + + namespace ut { + + /* compile-time test: + * verify we can use a basis_unit as a non-type template parameter + * we will need this for quantity> + */ + template + constexpr bu_abbrev_type bu_mpl_abbrev = bu_abbrev(bu); + + TEST_CASE("basis_unit", "[basis_unit]") { + static_assert(bu_mpl_abbrev == bu_abbrev(bu::gram)); + REQUIRE(bu_mpl_abbrev == bu_abbrev(bu::gram)); + } /*TEST_CASE(basis_unit)*/ + + TEST_CASE("basis_unit1", "[basis_unit]") { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.basis_unit1")); + + static_assert(native_unit2_v[static_cast(dim::mass)].native_dim() == dim::mass); + static_assert(native_unit2_v[static_cast(dim::distance)].native_dim() == dim::distance); + static_assert(native_unit2_v[static_cast(dim::time)].native_dim() == dim::time); + static_assert(native_unit2_v[static_cast(dim::time)].native_dim() == dim::time); + static_assert(native_unit2_v[static_cast(dim::currency)].native_dim() == dim::currency); + static_assert(native_unit2_v[static_cast(dim::price)].native_dim() == dim::price); + + log && log(xtag("mass*10^3", bu_abbrev(bu::kilogram))); + + static_assert(bu_abbrev(bu::kilogram) + == bu_abbrev_type::from_chars("kg")); + + log && log("---------------------"); + + /* note: using CHECK to make test show up in coverage */ + +# define REQUIRE_x2(x) static_assert(x); REQUIRE(x) + + log && log(xtag("mass", bu_abbrev(bu::meter))); + + REQUIRE_x2(bu_abbrev(bu::picogram) == bu_abbrev_type::from_chars("pg")); + REQUIRE_x2(bu_abbrev(bu::nanogram) + == bu_abbrev_type::from_chars("ng")); + REQUIRE_x2(bu_abbrev(bu::microgram) + == bu_abbrev_type::from_chars("ug")); + REQUIRE_x2(bu_abbrev(bu::milligram) + == bu_abbrev_type::from_chars("mg")); + REQUIRE_x2(bu_abbrev(bu::gram) + == bu_abbrev_type::from_chars("g")); + REQUIRE_x2(bu_abbrev(bu::kilogram) + == bu_abbrev_type::from_chars("kg")); + REQUIRE_x2(bu_abbrev(bu::tonne) + == bu_abbrev_type::from_chars("t")); + REQUIRE_x2(bu_abbrev(bu::kilotonne) + == bu_abbrev_type::from_chars("kt")); + REQUIRE_x2(bu_abbrev(bu::megatonne) + == bu_abbrev_type::from_chars("Mt")); + REQUIRE_x2(bu_abbrev(bu::gigatonne) + == bu_abbrev_type::from_chars("Gt")); + + log && log(xtag("distance", bu_abbrev(bu::meter))); + + REQUIRE_x2(bu_abbrev(bu::picometre) + == bu_abbrev_type::from_chars("pm")); + REQUIRE_x2(bu_abbrev(bu::nanometre) + == bu_abbrev_type::from_chars("nm")); + REQUIRE_x2(bu_abbrev(bu::micrometre) + == bu_abbrev_type::from_chars("um")); + REQUIRE_x2(bu_abbrev(bu::millimetre) + == bu_abbrev_type::from_chars("mm")); + REQUIRE_x2(bu_abbrev(bu::metre) + == bu_abbrev_type::from_chars("m")); + REQUIRE_x2(bu_abbrev(bu::kilometre) + == bu_abbrev_type::from_chars("km")); + REQUIRE_x2(bu_abbrev(bu::megametre) + == bu_abbrev_type::from_chars("Mm")); + REQUIRE_x2(bu_abbrev(bu::gigametre) + == bu_abbrev_type::from_chars("Gm")); + + REQUIRE_x2(bu_abbrev(bu::picometer) + == bu_abbrev_type::from_chars("pm")); + REQUIRE_x2(bu_abbrev(bu::nanometer) + == bu_abbrev_type::from_chars("nm")); + REQUIRE_x2(bu_abbrev(bu::micrometer) + == bu_abbrev_type::from_chars("um")); + REQUIRE_x2(bu_abbrev(bu::millimeter) + == bu_abbrev_type::from_chars("mm")); + REQUIRE_x2(bu_abbrev(bu::meter) + == bu_abbrev_type::from_chars("m")); + REQUIRE_x2(bu_abbrev(bu::kilometer) + == bu_abbrev_type::from_chars("km")); + REQUIRE_x2(bu_abbrev(bu::megameter) + == bu_abbrev_type::from_chars("Mm")); + REQUIRE_x2(bu_abbrev(bu::gigameter) + == bu_abbrev_type::from_chars("Gm")); + + REQUIRE_x2(bu_abbrev(bu::lightsecond) == flatstring("lsec")); + REQUIRE_x2(bu_abbrev(bu::astronomicalunit) == flatstring("AU")); + + REQUIRE_x2(bu_abbrev(bu::inch) == flatstring("in")); + REQUIRE_x2(bu_abbrev(bu::foot) == flatstring("ft")); + REQUIRE_x2(bu_abbrev(bu::yard) == flatstring("yd")); + REQUIRE_x2(bu_abbrev(bu::mile) == flatstring("mi")); + + log && log(xtag("time", bu_abbrev(bu::second))); + + REQUIRE_x2(bu_abbrev(bu::picosecond) == bu_abbrev_type::from_chars("ps")); + REQUIRE_x2(bu_abbrev(bu::nanosecond) == bu_abbrev_type::from_chars("ns")); + REQUIRE_x2(bu_abbrev(bu::microsecond) == bu_abbrev_type::from_chars("us")); + REQUIRE_x2(bu_abbrev(bu::millisecond) == bu_abbrev_type::from_chars("ms")); + REQUIRE_x2(bu_abbrev(bu::second) == bu_abbrev_type::from_chars("s")); + REQUIRE_x2(bu_abbrev(bu::minute) == bu_abbrev_type::from_chars("min")); + REQUIRE_x2(bu_abbrev(bu::hour) == bu_abbrev_type::from_chars("hr")); + REQUIRE_x2(bu_abbrev(bu::day) == bu_abbrev_type::from_chars("dy")); + REQUIRE_x2(bu_abbrev(bu::week) == bu_abbrev_type::from_chars("wk")); + REQUIRE_x2(bu_abbrev(bu::month) == bu_abbrev_type::from_chars("mo")); + + REQUIRE_x2(bu_abbrev(bu::year) == bu_abbrev_type::from_chars("yr")); + REQUIRE_x2(bu_abbrev(bu::year250) == bu_abbrev_type::from_chars("yr250")); + REQUIRE_x2(bu_abbrev(bu::year360) == bu_abbrev_type::from_chars("yr360")); + REQUIRE_x2(bu_abbrev(bu::year365) == bu_abbrev_type::from_chars("yr365")); + + log && log(xtag("currency", bu_abbrev(bu::currency))); + + REQUIRE_x2(bu_abbrev(bu::currency) == flatstring("ccy")); + + log && log(xtag("price", bu_abbrev(bu::price))); + + REQUIRE_x2(bu_abbrev(bu::price) == flatstring("px")); + +# undef REQUIRE_x2 + + } /*TEST_CASE(basis_unit1)*/ + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end bu.test.cpp */ diff --git a/xo-unit/utest/bpu.test.cpp b/xo-unit/utest/bpu.test.cpp new file mode 100644 index 00000000..666c67ca --- /dev/null +++ b/xo-unit/utest/bpu.test.cpp @@ -0,0 +1,95 @@ +/* @file bpu.test.cpp */ + +#include "xo/unit/bpu.hpp" +#include "xo/indentlog/scope.hpp" +#include + +namespace xo { + using xo::qty::abbrev::bpu_abbrev; + using xo::qty::abbrev::flatstring_from_exponent; + + namespace qty { + using bpu64_type = bpu; + + /* compile-time test: + * verify we can use a bpu64_type instance as a non-type template parameter. + * Will need this for quantity> + */ + template + constexpr bpu_abbrev_type bpu_mpl_abbrev = bpu.abbrev(); + + TEST_CASE("bpu", "[bpu]") { + static_assert(bpu_mpl_abbrev + == bpu64_type::unit_power(detail::bu::gram).abbrev()); + REQUIRE(bpu_mpl_abbrev + == bpu64_type::unit_power(detail::bu::gram).abbrev()); + } /*TEST_CASE(bpu)*/ + + TEST_CASE("flatstring_from_exponent", "[flatstring_from_exponent]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.flatstring_from_exponent")); + + log && log(xtag("^-3", flatstring_from_exponent(power_ratio_type(-3,1)))); + log && log(xtag("^-2", flatstring_from_exponent(power_ratio_type(-2,1)))); + log && log(xtag("^-1", flatstring_from_exponent(power_ratio_type(-1,1)))); + log && log(xtag("^-1/2", flatstring_from_exponent(power_ratio_type(-1,2)))); + log && log(xtag("^0", flatstring_from_exponent(power_ratio_type(0,1)))); + log && log(xtag("^1/2", flatstring_from_exponent(power_ratio_type(1,2)))); + log && log(xtag("^1", flatstring_from_exponent(power_ratio_type(1,1)))); + log && log(xtag("^2", flatstring_from_exponent(power_ratio_type(2,1)))); + log && log(xtag("^3", flatstring_from_exponent(power_ratio_type(3,1)))); + + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(-3,1))) + == flatstring<5>::from_flatstring(flatstring("^-3"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(-2,1))) + == flatstring<5>::from_flatstring(flatstring("^-2"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(-1,1))) + == flatstring<5>::from_flatstring(flatstring("^-1"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(-1,2))) + == flatstring<5>::from_flatstring(flatstring("^(-1/2)"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(1,2))) + == flatstring<5>::from_flatstring(flatstring("^(1/2)"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(1,1))) + == flatstring<5>::from_flatstring(flatstring(""))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(2,1))) + == flatstring<5>::from_flatstring(flatstring("^2"))); + static_assert(flatstring<5>::from_flatstring(flatstring_from_exponent(power_ratio_type(3,1))) + == flatstring<5>::from_flatstring(flatstring("^3"))); + } /*TEST_CASE(flatstring_from_exponent)*/ + + TEST_CASE("bpu_abbrev", "[bpu_abbrev]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.bpu2_assemble_abbrev")); + + log && log(xtag("1/(kg*kg)", bpu_abbrev(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(-2, 1)))); + log && log(xtag("1/kg", bpu_abbrev(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(-1, 1)))); + log && log(xtag("kg", bpu_abbrev(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)))); + log && log(xtag("kg*kg", bpu_abbrev(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(2, 1)))); + + static_assert(bpu(dim::mass, scalefactor_ratio_type(1, 1), power_ratio_type(1, 1)).abbrev() + == bpu_abbrev_type::from_chars("g")); + static_assert(bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)).abbrev() + == bpu_abbrev_type::from_chars("kg")); + static_assert(bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(-1, 1)).abbrev() + == bpu_abbrev_type::from_chars("kg^-1")); + static_assert(bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(-2, 1)).abbrev() + == bpu_abbrev_type::from_chars("kg^-2")); + + static_assert(bpu(dim::time, scalefactor_ratio_type(60, 1), power_ratio_type(-2, 1)).abbrev() + == bpu_abbrev_type::from_chars("min^-2")); + static_assert(bpu(dim::time, scalefactor_ratio_type(3600, 1), power_ratio_type(-1, 1)).abbrev() + == bpu_abbrev_type::from_chars("hr^-1")); + static_assert(bpu(dim::time, scalefactor_ratio_type(24*3600, 1), power_ratio_type(-1, 1)).abbrev() + == bpu_abbrev_type::from_chars("dy^-1")); + static_assert(bpu(dim::time, scalefactor_ratio_type(360*24*3600, 1), power_ratio_type(-1, 1)).abbrev() + == bpu_abbrev_type::from_chars("yr360^-1")); + static_assert(bpu(dim::time, scalefactor_ratio_type(360*24*3600, 1), power_ratio_type(-1, 2)).abbrev() + == bpu_abbrev_type::from_chars("yr360^(-1/2)")); + } /*TEST_CASE(bpu_abbrev)*/ + + } /*namespace qty*/ +} /*namespace xo*/ + +/* end bpu.test.cpp */ diff --git a/xo-unit/utest/mpl_quantity.test.cpp b/xo-unit/utest/mpl_quantity.test.cpp new file mode 100644 index 00000000..aefab2ae --- /dev/null +++ b/xo-unit/utest/mpl_quantity.test.cpp @@ -0,0 +1,862 @@ +/* @file quantity.test.cpp */ + +#include "xo/unit/mpl/quantity.hpp" +#include "xo/reflect/Reflect.hpp" +//#include +//#include +#include +#include +#include +#include + +namespace xo { + using xo::unit::quantity; + + using xo::unit::qty::kilograms; + + using xo::unit::qty::meters; + using xo::unit::qty::kilometers; + + using xo::unit::qty::milliseconds; + using xo::unit::qty::seconds; + using xo::unit::qty::minutes; + using xo::unit::qty::hours; + using xo::unit::qty::volatility30d; + using xo::unit::qty::volatility250d; + + using xo::unit::unit_find_bpu_t; + using xo::unit::unit_conversion_factor_t; + using xo::unit::unit_cartesian_product_t; + using xo::unit::unit_cartesian_product; + using xo::unit::unit_invert_t; + using xo::unit::unit_abbrev_v; + using xo::unit::same_dimension_v; + using xo::unit::dim; + + using xo::unit::from_ratio; + using xo::unit::stringliteral_from_ratio; + using xo::unit::ratio2str_aux; + using xo::unit::cstr_from_ratio; + + using xo::reflect::Reflect; + + namespace units = xo::unit::units; + + namespace ut { + /* use 'testcase' snippet to add test cases here */ + TEST_CASE("quantity", "[quantity]") { + //constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + //scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.quantity"), xtag("foo", foo), ...); + //log && log("(A)", xtag("foo", foo)); + + quantity t = seconds(1L); + + REQUIRE(t.scale() == 1); + + static_assert(t.basis_power == 1); + static_assert(t.basis_power == 0); + } /*TEST_CASE(quantity)*/ + + TEST_CASE("add1", "[quantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.add1")); + + quantity t1 = seconds(1); + quantity t2 = seconds(2); + + static_assert(std::same_as); + static_assert(std::same_as); + + auto sum = t1 + t2; + + CHECK(strcmp(sum.unit_cstr(), "s") == 0); + + static_assert(std::same_as); + static_assert(t1.basis_power == 1); + static_assert(t2.basis_power == 1); + + REQUIRE(sum.scale() == 3); + + } /*TEST_CASE(add1)*/ + + TEST_CASE("add2", "[quantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.add2")); + + quantity t1 = seconds(1); + { + CHECK(strcmp(t1.unit_cstr(), "s") == 0); + CHECK(t1.scale() == 1); + } + + auto m2 = minutes(2); + + { + static_assert(m2.basis_power == 1); + + log && log(xtag("m2.scale", m2.scale()), xtag("m2.unit", m2.unit_cstr())); + + CHECK(m2.scale() == 2); + CHECK(strcmp(m2.unit_cstr(), "min") == 0); + } + + { + auto m2_sec = m2.in_units_of(); + + static_assert(std::same_as); + + log && log(XTAG(m2_sec)); + + CHECK(m2_sec == 120); + } + + quantity t2 = m2; + { + auto sum = t1 + t2; + + static_assert(std::same_as); + static_assert(sum.basis_power == 1); + + log && log(xtag("t1.unit", t1.unit_cstr()), xtag("t2.unit", t2.unit_cstr())); + log && log(xtag("sum.unit", sum.unit_cstr())); + + CHECK(strcmp(t2.unit_cstr(), "s") == 0); + CHECK(strcmp(sum.unit_cstr(), "s") == 0); + CHECK(sum.scale() == 121); + } + } /*TEST_CASE(add2)*/ + + TEST_CASE("add3", "[quantity]") { + quantity t1 = seconds(1); + quantity t2 = minutes(2); + + /* sum will take unit from lhs argument to + */ + auto sum = t1 + t2; + + static_assert(sum.basis_power == 1); + static_assert(std::same_as); + + REQUIRE(sum.scale() == 121); + } /*TEST_CASE(add3)*/ + + TEST_CASE("add4", "[quantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.add4")); + + using u_kgps_result = unit_cartesian_product>; + using u_kgps = u_kgps_result::exact_unit_type; + using u_gpm_result = unit_cartesian_product>; + using u_gpm = u_gpm_result::exact_unit_type; + { + static_assert(u_kgps_result::c_scalefactor_inexact == 1.0); + + static_assert(std::same_as::power_type, std::ratio<1>>); + static_assert(std::same_as::power_type, std::ratio<-1>>); + static_assert(std::same_as::power_type, std::ratio<1>>); + static_assert(std::same_as::power_type, std::ratio<-1>>); + + log && log(xtag("u_kgps", unit_abbrev_v.c_str())); + log && log(xtag("u_gpm", unit_abbrev_v.c_str())); + + CHECK(strcmp(unit_abbrev_v.c_str(), "kg.s^-1") == 0); + CHECK(strcmp(unit_abbrev_v.c_str(), "g.min^-1") == 0); + + static_assert(same_dimension_v); + } + + using convert_type = unit_conversion_factor_t; + { + log && log(xtag("u_kgps->u_gpm", cstr_from_ratio())); + + CHECK(strcmp(cstr_from_ratio(), "60000") == 0); + CHECK(from_ratio() == 60000); + } + + /* note: in practice probably write + * kilograms(0.1) / seconds(1); + * but + * 1. don't want to exercise quantity {*,/} here; + * 2. want to force unit representation + */ + auto q1 = quantity::promote(0.1); + auto q2 = quantity(); + { + q2 = q1; + + static_assert(q1.basis_power == 1); + static_assert(q1.basis_power == -1); + static_assert(q2.basis_power == 1); + static_assert(q2.basis_power == -1); + + log && log(XTAG(q1), XTAG(q2)); + + CHECK(strcmp(q1.unit_cstr(), "kg.s^-1") == 0); + CHECK(q1.scale() == 0.1); + + CHECK(strcmp(q2.unit_cstr(), "g.min^-1") == 0); + CHECK(q2.scale() == 6000.0); + } + } /*TEST_CASE(add4)*/ + + TEST_CASE("add5", "[quantity][fractional_dimension]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.add5")); + //log && log("(A)", xtag("foo", foo)); + + auto vol_250d = volatility250d(0.2); + { + log && log(xtag("vol_250d", vol_250d)); + + CHECK(strcmp(vol_250d.unit_cstr(), "yr250^-(1/2)") == 0); + CHECK(vol_250d.scale() == 0.2); + } + + /* scaling factor from 30-day vol to 250-day vol is sqrt(250/30) ~ 2.88675 + * so 0.1 -> 0.288675 + */ + auto vol_30d = volatility30d(0.1); + { + log && log(xtag("vol_30d", vol_30d)); + + CHECK(strcmp(vol_30d.unit_cstr(), "mo^-(1/2)") == 0); + CHECK(vol_30d.scale() == Approx(0.1).epsilon(1e-6)); + } + + /* conversion from monthly vol to (250-day) annual vol */ + + using u_vol250d = units::volatility_250d; + { + quantity q = vol_30d; + + log && log(xtag("q", q)); + + CHECK(strcmp(q.unit_cstr(), "yr250^-(1/2)") == 0); + CHECK(q.scale() == Approx(0.288675).epsilon(1e-6)); + + } + + { + auto sum = vol_250d + vol_30d; + + static_assert(sum.basis_power == -0.5); + + log && log(XTAG(sum)); + + CHECK(strcmp(sum.unit_cstr(), "yr250^-(1/2)") == 0); + /* 0.1mo^-(1/2) ~ 0.288675yr250^-(1/2) */ + CHECK(sum.scale() == Approx(0.4886751).epsilon(1e-6)); + } + } /*TEST_CASE(add5)*/ + + + TEST_CASE("mult1", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.mult1")); + //log && log("(A)", xtag("foo", foo)); + + auto q0 = milliseconds(5); + auto q1 = seconds(60); + auto q2 = minutes(1); + + { + auto r = q0 * q1; + + static_assert(r.basis_power == 2); + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q0*q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* taking unit from LHS */ + REQUIRE(strcmp(r.unit_cstr(), "ms^2") == 0); + REQUIRE(r.scale() == 300000); + } + + { + auto r = q1 * q2; + + static_assert(r.basis_power == 2); + + log && log(xtag("q1", q1), xtag("q2", q2), xtag("q1*q2", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* taking unit from LHS */ + REQUIRE(strcmp(r.unit_cstr(), "s^2") == 0); + REQUIRE(r.scale() == 3600); + } + + { + auto r = q2 * q1; + + static_assert(r.basis_power == 2); + + log && log(xtag("q1", q1), xtag("q2", q2), xtag("r=q2*q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* taking unit from LHS */ + CHECK(strcmp(r.unit_cstr(), "min^2") == 0); + CHECK(r.scale() == 1); + } + + { + auto r = q2 * 60; + + static_assert(r.basis_power == 1); + static_assert(std::same_as); + + log && log(xtag("q2*60", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + CHECK(strcmp(r.unit_cstr(), "min") == 0); + CHECK(r.scale() == 60); + } + + { + auto r = q2 * 60U; + + static_assert(r.basis_power == 1); + static_assert(std::same_as); + + log && log(xtag("q2*60U", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + CHECK(strcmp(r.unit_cstr(), "min") == 0); + CHECK(r.scale() == 60U); + } + + { + auto r = (q2 * 60.5); + + static_assert(r.basis_power == 1); + + /* verify dimension */ + static_assert(std::same_as); + + log && log(xtag("q2*60.5", q2*60.5)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + REQUIRE(strcmp(r.unit_cstr(), "min") == 0); + REQUIRE(r.scale() == 60.5); + } + + { + log && log(xtag("q2*60.5f", q2*60.5f)); + + auto r = (q2 * 60.5f); + + /* verify dimension */ + static_assert(r.basis_power == 1); + static_assert(std::same_as); + + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + REQUIRE(strcmp(r.unit_cstr(), "min") == 0); + REQUIRE(r.scale() == 60.5f); + } + + { + auto r = 60 * q2; + + static_assert(r.basis_power == 1); + static_assert(std::same_as); + + log && log(xtag("60*q2", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + CHECK(strcmp(r.unit_cstr(), "min") == 0); + CHECK(r.scale() == 60); + } + + { + log && log(xtag("60.5*q2", 60.5*q2)); + + auto r = 60.5 * q2; + + static_assert(r.basis_power == 1); + + log && log(xtag("r.type", Reflect::require()->canonical_name())); + static_assert(std::same_as); + + /* preserve units of existing quantity */ + CHECK(strcmp(r.unit_cstr(), "min") == 0); + CHECK(r.scale() == 60.5); + } + + { + log && log(xtag("60.5f*q2", 60.5f*q2)); + + auto r = 60.5f * q2; + + static_assert(r.basis_power == 1); + static_assert(std::same_as); + + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* preserve units of existing quantity */ + CHECK(strcmp(r.unit_cstr(), "min") == 0); + CHECK(r.scale() == 60.5); + } + } /*TEST_CASE(mult1)*/ + + TEST_CASE("div1", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.div1")); + //log && log("(A)", xtag("foo", foo)); + + auto q0 = milliseconds(5); + auto q1 = milliseconds(10); + + { + /* repr_type adopts argument to milliseconds() */ + static_assert(std::same_as); + static_assert(std::same_as); + + auto r = q0/q1; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q0/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimensionless + no type promotion */ + static_assert(std::same_as); + /* verify scale (truncate)*/ + REQUIRE(r == 0); + } + + auto q0p = milliseconds(5.0); + + { + static_assert(std::same_as); + + auto r = q0p/q1; + static_assert(std::same_as); + + log && log(XTAG(q0p), xtag("q0p/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(std::same_as); + + /* verify scale */ + REQUIRE(r == 0.5); + } + + auto r1 = 1.0 / q0; + + { + log && log(XTAG(q0), xtag("r1=1.0/q0", r1)); + + /* verify dimension */ + static_assert(r1.basis_power == -1); + + /* verify scale */ + REQUIRE(r1.scale() == 0.2); + } + } /*TEST_CASE(div1)*/ + + TEST_CASE("div2", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.div2")); + + auto q0 = milliseconds(5); + auto q1 = milliseconds(20.0); + + { + auto r = q0/q1; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q0/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(std::same_as); + + /* verify scale */ + REQUIRE(r == 0.25); + } + + { + auto r = q1/q0; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q1/q0", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(std::same_as); + + /* verify scale */ + REQUIRE(r == 4.0); + } + + { + auto r = q0/(q1*q1); + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q0/(q1*q1)", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(r.basis_power == -1); + + /* verify scale */ + REQUIRE(r.scale() == 0.0125); + } + + { + auto r = (q0*q0)/q1; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("(q0*q0)/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(r.basis_power == 1); + + /* verify scale */ + REQUIRE(r.scale() == 1.25); + } + + } /*TEST_CASE(div2)*/ + + TEST_CASE("div3", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.div3")); + + auto q0 = milliseconds(5); + auto q1 = milliseconds(20.0); + + { + auto r = q0/q1; + + log && log(XTAG(q0), XTAG(q1), xtag("q0/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(std::same_as); + + /* verify scale */ + REQUIRE(r == 0.25); + } + + { + auto r = q1/q0; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q1/q0", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(std::same_as); + + /* verify scale */ + REQUIRE(r == 4.0); + } + + { + auto r = q0/(q1*q1); + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("q0/(q1*q1)", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(r.basis_power == -1); + + /* verify scale */ + REQUIRE(r.scale() == 0.0125); + } + + { + auto r = (q0*q0)/q1; + + log && log(xtag("q0", q0), xtag("q1", q1), xtag("(q0*q0)/q1", r)); + log && log(xtag("r.type", Reflect::require()->canonical_name())); + + /* verify dimension */ + static_assert(r.basis_power == 1); + + /* verify scale */ + REQUIRE(r.scale() == 1.25); + } + + } /*TEST_CASE(div3)*/ + + TEST_CASE("div4", "[quantity]") { + /* test with exact scalefactor */ + + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.div4")); + //log && log("(A)", xtag("foo", foo)); + + auto q1 = milliseconds(1); + auto q2 = minutes(1); + + auto r = q1 / q2.with_repr(); + + /* 0.1/sqrt(30dy) ~ 0.288675/sqrt(250dy), + * so q1/q2 ~ 0.6928 + */ + + log && log(XTAG(q1), XTAG(q2), xtag("q1/q2", r)); + + /* verify dimensionless result */ + static_assert(std::same_as); + + /* verify scale of result */ + CHECK(r == Approx(0.00001666667).epsilon(1e-6)); + + } /*TEST_CASE(div4)*/ + + TEST_CASE("div5", "[quantity]") { + /* test with inexact scalefactor */ + + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.div5")); + //log && log("(A)", xtag("foo", foo)); + + auto q1 = volatility250d(0.2); + auto q2 = volatility30d(0.1); + + auto r = q1/q2; + + /* 0.1/sqrt(30dy) ~ 0.288675/sqrt(250dy), + * so q1/q2 ~ 0.6928 + */ + + log && log(XTAG(q1), XTAG(q2), XTAG(q1/q2)); + + /* verify dimensionless result */ + static_assert(std::same_as); + + /* verify scale of result */ + CHECK(r == Approx(0.692820323).epsilon(1e-6)); + + } /*TEST_CASE(div5)*/ + + TEST_CASE("muldiv5", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.muldiv5")); + //log && log("(A)", xtag("foo", foo)); + + auto t = milliseconds(10); + auto m = kilograms(2.5); + + auto a = m / (t * t); + + /* 0.1/sqrt(30dy) ~ 0.288675/sqrt(250dy), + * so q1/q2 ~ 0.6928 + */ + + log && log(XTAG(m), XTAG(t), xtag("a=m.t^-2", a)); + + /* verify dimensions of result + sticky units */ + CHECK(strcmp(t.unit_cstr(), "ms") == 0); + CHECK(strcmp(m.unit_cstr(), "kg") == 0); + CHECK(strcmp(a.unit_cstr(), "kg.ms^-2") == 0); + + CHECK(a.scale() == 0.025); + + /* verify scale of result */ + //CHECK(r == Approx(0.692820323).epsilon(1e-6)); + + } /*TEST_CASE(muldiv5)*/ + + TEST_CASE("rescale", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.rescale")); + //log && log("(A)", xtag("foo", foo)); + + auto q = kilograms(150.0) / minutes(1); /* 150.0kg.min^-1 */ + + CHECK(strcmp(q.unit_cstr(), "kg.min^-1") == 0); + CHECK(q.scale() == 150.0); + + log && log(XTAG(q)); + + namespace u = xo::unit::units; + + auto q1 = q.with_basis_unit(); /* 0.0025kg.ms^-1 */ + + CHECK(strcmp(q1.unit_cstr(), "kg.ms^-1") == 0); + CHECK(q1.scale() == 0.0025); + + log && log(XTAG(q1)); + + auto q2 = q1.with_basis_unit(); /* 2.5g.ms^-1 */ + + CHECK(strcmp(q2.unit_cstr(), "g.ms^-1") == 0); + CHECK(q2.scale() == 2.5); + + log && log(XTAG(q2)); + + auto q3 = q2.with_basis_unit(); /* 2500g.s^-1 */ + + CHECK(strcmp(q3.unit_cstr(), "g.s^-1") == 0); + CHECK(q3.scale() == 2500.0); + + log && log(XTAG(q3)); + } /*TEST_CASE(rescale)*/ + + TEST_CASE("rescale2", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.rescale2")); + //log && log("(A)", xtag("foo", foo)); + + namespace u = xo::unit::unit_qty; + + auto q1 = kilometers(150.0) / u::hour; + auto q2 = q1.with_units_from(u::meter / u::second); + + log && log(XTAG(q1), XTAG(q2)); + } /*TEST_CASE(rescale2)*/ + + TEST_CASE("compare1", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.compare1")); + //log && log("(A)", xtag("foo", foo)); + + namespace u = xo::unit::unit_qty; + + auto q1 = kilometers(150.0) / u::hour; + auto q2 = kilometers(100.0) / u::hour; + + CHECK(is_gt(q1 <=> q2)); + CHECK(is_eq(q1 <=> q1)); + CHECK(is_lt(q2 <=> q1)); + CHECK(q1 == q1); + CHECK(q1 != q2); + CHECK(q1 >= q1); + CHECK(q1 <= q1); + + CHECK(q1 > q2); + CHECK(q1 >= q2); + + CHECK(q2 < q1); + CHECK(q2 <= q1); + + log && log(XTAG(q1), XTAG(q2), XTAG(is_gt(q1<=>q2))); + } /*TEST_CASE(compare1)*/ + + TEST_CASE("compare2", "[quantity]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.compare2")); + //log && log("(A)", xtag("foo", foo)); + + namespace u = xo::unit::unit_qty; + + auto q1 = kilometers(150.0) / u::hour; + auto q2 = meters(30.0) / u::second; + + CHECK(is_gt(q1 <=> q2)); + CHECK(is_eq(q1 <=> q1)); + CHECK(is_lt(q2 <=> q1)); + CHECK(q1 == q1); + CHECK(q1 != q2); + CHECK(q1 >= q1); + CHECK(q1 <= q1); + + CHECK(q1 > q2); + CHECK(q1 >= q2); + + CHECK(q2 < q1); + CHECK(q2 <= q1); + + log && log(XTAG(q1), XTAG(q2), XTAG(is_gt(q1<=>q2))); + } /*TEST_CASE(compare2)*/ + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end quantity.test.cpp */ diff --git a/xo-unit/utest/mpl_unit.test.cpp b/xo-unit/utest/mpl_unit.test.cpp new file mode 100644 index 00000000..a72c32d8 --- /dev/null +++ b/xo-unit/utest/mpl_unit.test.cpp @@ -0,0 +1,380 @@ +/* @file mpl_unit.test.cpp */ + +//#include "native_bpu.hpp" +#include "xo/unit/mpl/quantity.hpp" +//#include "xo/unit/unit.hpp" +//#include "xo/unit/Quantity2_iostream.hpp" +//#include "xo/unit/Quantity2.hpp" +//#include "xo/unit/scaled_unit_iostream.hpp" +//#include "xo/unit/natural_unit.hpp" +//#include "xo/unit/natural_unit_iostream.hpp" +//#include "xo/unit/bpu_store.hpp" +//#include "xo/unit/native_bpu2.hpp" +//#include "xo/unit/native_bpu2_iostream.hpp" +//#include "xo/unit/basis_unit2.hpp" +//#include "xo/unit/dim_util2.hpp" +#include "xo/reflect/Reflect.hpp" +//#include "xo/cxxutil/demangle.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + namespace ut { + /* compile-time tests */ + + using xo::reflect::Reflect; + + //namespace su2 = xo::unit::su2; + //using xo::unit::Quantity2; + using xo::unit::dim; + //using xo::unit::basis_unit2_abbrev_type; + //using xo::unit::native_unit2; + //using xo::unit::native_unit2_v; + //using xo::unit::scalefactor_ratio_type; + //using xo::unit::units::scaled_native_unit2_abbrev; + //using xo::unit::units::scaled_native_unit2_abbrev_v; + //using xo::unit::basis_unit2; + //using xo::unit::abbrev::basis_unit2_abbrev;; + //using xo::unit::bpu2_abbrev_type; + //using xo::unit::abbrev::bpu2_abbrev; + //using xo::unit::basis_unit2_store; + //using xo::unit::power_ratio_type; + //using xo::unit::abbrev::flatstring_from_exponent; + //using xo::unit::bpu2; + //using xo::unit::detail::bpu2_rescale; + //using xo::unit::detail::bpu2_product; + //using xo::unit::natural_unit; + //using xo::unit::bpu_array_maker; + //using xo::unit::detail::nu_product; + //using xo::unit::unit_qty; + + using xo::unit::native_unit_abbrev_v; + using xo::unit::units::scaled_native_unit_abbrev_v; + //using xo::unit::native_dim_abbrev; + using xo::unit::stringliteral_compare; + using xo::unit::literal_size_v; + using xo::unit::stringliteral_from_digit; + using xo::unit::stringliteral_from_int_v; + using xo::unit::stringliteral; +#ifndef __clang__ + using xo::unit::stringliteral_concat; + using xo::unit::stringliteral_from_ratio; + using xo::unit::bpu_assemble_abbrev_helper; + using xo::unit::bpu_assemble_abbrev; +#endif + using xo::unit::bpu_node; + using xo::unit::wrap_unit; + using xo::unit::unit_abbrev_v; + //using xo::unit::dim_abbrev_v; + using xo::unit::di_cartesian_product; + using xo::unit::di_cartesian_product1; + using xo::unit::unit_cartesian_product_t; + using xo::unit::bpu_cartesian_product; + using xo::unit::bpu_cartesian_product_helper; + using xo::unit::unit_invert_t; + using xo::unit::units::gram; + using xo::unit::units::second; + using xo::print::ccs; + + template + int unused() + { + return 1; + } + + template + constexpr bool unused_same(typename std::enable_if_t::value, bool> result = true) + { + return result; + } + + TEST_CASE("native_unit_abbrev", "[native_dim_abbrev]") { + constexpr bool c_debug_flag = true; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.native_dim_abbrev")); + //log && log("(A)", xtag("foo", foo)); + + /* NOTE: the .value_ expression below will fail to compile if missing specialization for + * native_dim_abbrev on native_dim_id::foo; that's the point :) + */ + + REQUIRE(strcmp(scaled_native_unit_abbrev_v>.value_, "g") == 0); + REQUIRE(strcmp(scaled_native_unit_abbrev_v>.value_, "s") == 0); + REQUIRE(strcmp(scaled_native_unit_abbrev_v>.value_, "ccy") == 0); + REQUIRE(strcmp(scaled_native_unit_abbrev_v>.value_, "px") == 0); + +#ifdef OBSOLETE + REQUIRE(strcmp(native_dim_abbrev().value_, "") != 0); + REQUIRE(strcmp(native_dim_abbrev().value_, "") != 0); + REQUIRE(strcmp(native_dim_abbrev().value_, "") != 0); + REQUIRE(strcmp(native_dim_abbrev().value_, "") != 0); +#endif + + static_assert(stringliteral_compare(stringliteral_from_digit(0), stringliteral("0")) == 0); + static_assert(stringliteral_compare(stringliteral_from_digit(1), stringliteral("1")) == 0); + static_assert(stringliteral_compare(stringliteral_from_digit(9), stringliteral("9")) == 0); + + static_assert(literal_size_v<0> == 1); + static_assert(literal_size_v<10> == 2); + static_assert(literal_size_v<99> == 2); + static_assert(literal_size_v<100> == 3); + static_assert(literal_size_v<999> == 3); + + static_assert(stringliteral_compare(stringliteral_from_int_v<0>(), stringliteral("0")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<9>(), stringliteral("9")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<1, 1, false>(), stringliteral("1")) == 0); + + + static_assert(stringliteral_compare(stringliteral_from_int_v<9, 1, false>(), stringliteral("9")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<9>(), stringliteral("9")) == 0); + + /* NOTE: clang16 complains starting here; gcc is fine */ + +#ifndef __clang__ + if constexpr (stringliteral_concat("a", "b").size() == 3) { + REQUIRE(true); + } else { + REQUIRE(false); + } + + static_assert(stringliteral_compare(stringliteral_concat("hello", " ", "world"), + stringliteral("hello world")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<10, 2, false>(), stringliteral("10")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<10>(), stringliteral("10")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<99, 2, false>(), stringliteral("99")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<99>(), stringliteral("99")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<100, 3, false>(), stringliteral("100")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<100>(), stringliteral("100")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<999, 3, false>(), stringliteral("999")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<999>(), stringliteral("999")) == 0); + + //std::cerr << "test=" << stringliteral_from_int_v<-1, 2, true>().value_ << std::endl; + + static_assert(stringliteral_compare(stringliteral_from_int_v<-1, 2, true>(), stringliteral("-1")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-1>(), stringliteral("-1")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<-9, 2, true>(), stringliteral("-9")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-9>(), stringliteral("-9")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<-10, 3, true>(), stringliteral("-10")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-10>(), stringliteral("-10")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<-99, 3, true>(), stringliteral("-99")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-99>(), stringliteral("-99")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<-100, 4, true>(), stringliteral("-100")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-100>(), stringliteral("-100")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_int_v<-999, 4, true>(), stringliteral("-999")) == 0); + static_assert(stringliteral_compare(stringliteral_from_int_v<-999>(), stringliteral("-999")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("(2/3)")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("(2/3)")) == 0); + + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-1")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-2")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-2")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-(3/2)")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-(3/2)")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("-(1/2)")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("(1/2)")) == 0); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("(3/2)")) == 0); + + //log && log(xtag("ratio<2>", stringliteral_from_ratio>().c_str())); + static_assert(stringliteral_compare(stringliteral_from_ratio>(), stringliteral("2")) == 0); + + static_assert(stringliteral_compare(bpu_assemble_abbrev_helper, std::ratio<1>>(), stringliteral("g")) == 0); + //log && log(xtag("s^(-1/2)", bpu_assemble_abbrev_helper, std::ratio<-1,2>>().c_str())); + static_assert(stringliteral_compare(bpu_assemble_abbrev_helper, std::ratio<-1,2>>(), stringliteral("s^-(1/2)")) == 0); + //stringliteral_compare(stringliteral_from_ratio>(), stringliteral("^2")) == 0); +#endif + + //static_assert(stringliteral_compare(stringliteral_from_int_v<10>(), obs::stringliteral("10")) == 0); + + //REQUIRE(strcmp(obs::stringliteral_from_digit(1).value_, "1") == 0); + //REQUIRE(strcmp(obs::ratio2str>().value_, "") == 0); + + } /*TEST_CASE(native_dim_abbrev)*/ + + TEST_CASE("dimension", "[dimension]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.dimension")); + //log && log("(A)", xtag("foo", foo)); + + using t1 = unit::bpu>; + + static_assert(t1::c_native_dim == unit::dim::currency); + static_assert(t1::power_type::num == 1); + static_assert(t1::power_type::den == 1); + + using t2 = unit::bpu, std::ratio<-1,2>>; + + static_assert(t2::c_native_dim == unit::dim::time); + static_assert(t2::power_type::num == -1); + static_assert(t2::power_type::den == 2); + + using dim1 = wrap_unit, bpu_node>; + using d1 = dim1::dim_type; /* ccy */ + REQUIRE(unused_same()); + REQUIRE(unused_same::power_unit_type, t1>()); +#ifdef NOT_USING + static_assert(unit::lo_basis_elt_of::c_lo_basis == t1::c_basis); +#endif + + static_assert(unit::native_lo_bwp_of::bwp_type::c_index == 0); + static_assert(unit::native_lo_bwp_of::bwp_type::c_basis == unit::dim::currency); + + + using dim2 = wrap_unit, bpu_node>; + using d2 = dim2::dim_type; /* t^(-1/2) */ + REQUIRE(unused_same()); + REQUIRE(unused_same::power_unit_type, t2>()); + static_assert(unit::native_lo_bwp_of::bwp_type::c_index == 0); + static_assert(unit::native_lo_bwp_of::bwp_type::c_basis == unit::dim::time); + + using dim3 = wrap_unit, bpu_node>>; + using d3 = dim3::dim_type; /* ccy.t^(-1/2) */ + REQUIRE(unused_same::power_unit_type, t1>()); + + { + using type = unit::lookup_bpu::power_unit_type; + //std::cerr << "unit::power_unit_of::power_unit_type" << xtag("type", reflect::type_name()) << std::endl; + + REQUIRE(unused_same()); + } + +#ifdef NOT_USING + static_assert(unit::lo_basis_elt_of::c_lo_basis == t2::c_basis); +#endif + + /* lowest is in pos 1, beacuse t2=time before t1=currency */ + static_assert(unit::native_lo_bwp_of::bwp_type::c_index == 1); + + static_assert(unused_same::dim_type, d2>()); + //using type = unit::without_elt::dim_type; + //std::cerr << "unit::without_elt::dim_type" << xtag("type", reflect::type_name()) << std::endl; + static_assert(unused_same::dim_type, d1>()); + + + using d3b = wrap_unit, + bpu_node>>::dim_type; /* t^(-1/2).ccy */ + //using d3b = unit::dimension_impl>; /* t^(-1/2).ccy */ + REQUIRE(unused_same::power_unit_type, t2>()); + REQUIRE(unused_same::power_unit_type, t1>()); + + /* lowest is in pos 0 */ + static_assert(unit::native_lo_bwp_of::bwp_type::c_index == 0); + + static_assert(unused_same::dim_type, d1>()); + static_assert(unused_same::dim_type, d2>()); + + static_assert(unused_same, unit::canonical_t>()); + + log && log(xtag("d1.abbrev", unit_abbrev_v.c_str())); + log && log(xtag("d2.abbrev", unit_abbrev_v.c_str())); + log && log(xtag("d3.abbrev", unit_abbrev_v.c_str())); + } + + TEST_CASE("dimension2", "[dimension2]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.dimension2")); + //log && log("(A)", xtag("foo", foo)); + + using di = di_cartesian_product; + + log && log(xtag("di", Reflect::require()->canonical_name())); + log && log(xtag("di::outer_scalefactor_type", Reflect::require()->canonical_name())); + log && log(xtag("di::bpu_list_type", Reflect::require()->canonical_name())); + + using u1 = unit_cartesian_product_t; + + log && log(xtag("u1", Reflect::require()->canonical_name())); + + log && log(xtag("u1", ccs(unit_abbrev_v.value_))); + } /*TEST_CASE(dimension2)*/ + + TEST_CASE("dimension3", "[dimension3]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.dimension3")); + //log && log("(A)", xtag("foo", foo)); + + using u1 = unit_invert_t; + + log && log(xtag("second^-1", Reflect::require()->canonical_name())); + log && log(xtag("u1", unit_abbrev_v.c_str())); + + REQUIRE(strcmp(unit_abbrev_v.c_str(), "s^-1") == 0); + + using u2 = second; + + log && log(xtag("second", Reflect::require()->canonical_name())); + log && log(xtag("u2", unit_abbrev_v.c_str())); + + using u1u2 = unit_cartesian_product_t; + + log && log(xtag("u1u2", Reflect::require()->canonical_name())); + +#ifdef NOT_USING + using di1 = d1::dim_type; + using di2 = d2::dim_type; + using di1di2 = di_cartesian_product::type; + + log && log(xtag("di1di2", Reflect::require()->canonical_name())); +#endif + + using f1 = u1::dim_type::front_type; + using r1 = u1::dim_type::rest_type; + using tmp = di_cartesian_product1; + + log && log(xtag("f1", Reflect::require()->canonical_name())); + log && log(xtag("r1", Reflect::require()->canonical_name())); + log && log(xtag("(f1.r1).outer_scalefactor_type", Reflect::require()->canonical_name())); + log && log(xtag("(f1.r1).bpu_list_type", Reflect::require()->canonical_name())); + + using tmp2 = bpu_cartesian_product; + + log && log(xtag("(f1.u2).outer_scalefactor_type", Reflect::require()->canonical_name())); + log && log(xtag("(f1.u2).bpu_list_type", Reflect::require()->canonical_name())); + + using f2 = u2::dim_type::front_type; + log && log(xtag("f2", Reflect::require()->canonical_name())); + + using tmp3 = bpu_cartesian_product_helper; + log && log(xtag("(f1.f2).outer_scalefactor_type", Reflect::require()->canonical_name())); + log && log(xtag("(f1.f2).bpu_list_type", Reflect::require()->canonical_name())); + } /*TEST_CASE(dimension3)*/ + + + } /*namespace ut*/ + +} /*namespace xo*/ + + +/* end dimension.test.cpp */ diff --git a/xo-unit/utest/natural_unit.test.cpp b/xo-unit/utest/natural_unit.test.cpp new file mode 100644 index 00000000..573e5dda --- /dev/null +++ b/xo-unit/utest/natural_unit.test.cpp @@ -0,0 +1,422 @@ +/* @file natural_unit.test.cpp */ + +#include "xo/unit/scaled_unit.hpp" +#include "xo/unit/scaled_unit_iostream.hpp" +#include "xo/unit/natural_unit.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + using xo::qty::detail::su_product; + using xo::qty::detail::su_ratio; + using xo::qty::detail::nu_ratio_inplace; + using xo::qty::detail::nu_maker; + using xo::qty::detail::bpu2_rescale; // -> nu_rescale or bpu_rescale + + namespace qty { + using nu64_type = natural_unit; + + /* compile-time test: + * verify we can use an nu64_type instance as a non-type template parameter. + * Will need this for quantity> + */ + template + constexpr nu_abbrev_type nu_mpl_abbrev = nu.abbrev(); + + TEST_CASE("natural_unit0", "[natural_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.natural_unit0")); + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::distance, scalefactor_ratio_type(1, 1000), power_ratio_type(2, 1)), + bpu(dim::mass, scalefactor_ratio_type(1, 1000), power_ratio_type(-1, 1)))); + + static_assert(v.n_bpu() == 2); + + log && log(xtag("v.abbrev", v.abbrev())); + + static_assert(v.abbrev().size() > 0); + static_assert(v.abbrev() == flatstring("mm^2.mg^-1")); + } + } /*TEST_CASE(natural_unit0)*/ + + TEST_CASE("natural_unit1", "[natural_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.natural_unit1")); + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::distance, scalefactor_ratio_type(1000, 1), power_ratio_type(2, 1)))); + + static_assert(v.n_bpu() == 1); + + log && log(xtag("v.abbrev", v.abbrev())); + + static_assert(v.abbrev().size() > 0); + static_assert(v.abbrev() == flatstring("km^2")); + } + } /*TEST_CASE(natural_unit1)*/ + + TEST_CASE("natural_unit2", "[natural_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.natural_unit2")); + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)), + bpu(dim::distance, scalefactor_ratio_type(1, 1), power_ratio_type(1, 1)), + bpu(dim::time, scalefactor_ratio_type(1, 1), power_ratio_type(-2, 1)))); + + static_assert(v.n_bpu() == 3); + + log && log(xtag("v.abbrev", v.abbrev())); + + static_assert(v.abbrev().size() > 0); + static_assert(v.abbrev() == flatstring("kg.m.s^-2")); + } + } /*TEST_CASE(natural_unit2)*/ + + TEST_CASE("natural_unit3", "[natural_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.natural_unit3")); + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)), + bpu(dim::distance, scalefactor_ratio_type(1, 1), power_ratio_type(1, 1)))); + + static_assert(v.n_bpu() == 2); + + log && log(xtag("v.abbrev", v.abbrev())); + + static_assert(v.abbrev().size() > 0); + static_assert(v.abbrev() == flatstring("kg.m")); + + { + natural_unit w = v; + + nu_ratio_inplace(&w, + bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1))); + + log && log(xtag("w.abbrev", w.abbrev())); + + REQUIRE(w.n_bpu() == 1); + REQUIRE(w[0].native_dim() == dim::distance); + REQUIRE(w.abbrev() == flatstring("m")); + } + + { + constexpr natural_unit w + = (nu_maker::make_nu + (bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)))); + + static_assert(w.n_bpu() == 1); + + log && log(xtag("w.abbrev", w.abbrev())); + + constexpr auto rr = su_ratio(v, w); + + log && log(xtag("rr", rr)); + + REQUIRE(rr.natural_unit_.n_bpu() == 1); + REQUIRE(rr.natural_unit_[0].native_dim() == dim::distance); + REQUIRE(rr.natural_unit_.abbrev() == flatstring("m")); + } + + { + constexpr natural_unit w + = (nu_maker::make_nu + (bpu(dim::time, scalefactor_ratio_type(1, 1), power_ratio_type(1, 1)))); + + static_assert(w.n_bpu() == 1); + + log && log(xtag("w.abbrev", w.abbrev())); + + constexpr auto rr = su_ratio(v, w); + + log && log(xtag("rr", rr)); + + REQUIRE(rr.natural_unit_.n_bpu() == 3); + REQUIRE(rr.natural_unit_[0].native_dim() == dim::mass); + REQUIRE(rr.natural_unit_[1].native_dim() == dim::distance); + REQUIRE(rr.natural_unit_[2].native_dim() == dim::time); + REQUIRE(rr.natural_unit_.abbrev() == flatstring("kg.m.s^-1")); + } + + { + natural_unit w = v; + + REQUIRE(w.n_bpu() == 2); + REQUIRE(w[0].native_dim() == dim::mass); + + nu_ratio_inplace(&w, + bpu(dim::time, scalefactor_ratio_type(1, 1), power_ratio_type(2, 1))); + + REQUIRE(w.n_bpu() == 3); + REQUIRE(w[0].native_dim() == dim::mass); + REQUIRE(w[1].native_dim() == dim::distance); + REQUIRE(w[2].native_dim() == dim::time); + + log && log(xtag("w.abbrev", w.abbrev())); + + REQUIRE(w.n_bpu() == 3); + REQUIRE(w.abbrev() == flatstring("kg.m.s^-2")); + } + } + } /*TEST_CASE(natural_unit3)*/ + + TEST_CASE("bpu_rescale", "[bpu_rescale]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.bpu_rescale")); + //log && log("(A)", xtag("foo", foo)); + + /* keep spelled-out test. Will generalize to fractional powers when c++26 available */ + { + constexpr auto p = power_ratio_type(1, 1); + + constexpr auto orig_bpu = bpu(dim::mass, + scalefactor_ratio_type(1000, 1), + power_ratio_type(1, 1)); + static_assert(orig_bpu.native_dim() == dim::mass); + + constexpr auto new_scalefactor = scalefactor_ratio_type(1000000, 1); + + constexpr auto mult = orig_bpu.scalefactor() / new_scalefactor; + static_assert(mult.num() == 1); + static_assert(mult.den() == 1000); + + constexpr auto p_floor = orig_bpu.power().floor(); + static_assert(p_floor == 1); + + constexpr auto p_frac = orig_bpu.power().frac().template convert_to(); + static_assert(p_frac == 0.0); + + constexpr auto outer_sf_exact = mult.power(p_floor); + static_assert(outer_sf_exact.num() == 1); + static_assert(outer_sf_exact.den() == 1000); + + constexpr auto mult_inexact = mult.template convert_to(); + static_assert(mult_inexact == 0.001); + + constexpr auto rr = bpu2_rescale(orig_bpu, scalefactor_ratio_type(1000000, 1)); + + static_assert(rr.bpu_rescaled_.power() == power_ratio_type(1,1)); + static_assert(rr.outer_scale_factor_ == outer_sf_exact); + static_assert(rr.outer_scale_sq_ == 1.0); + } + + /* keep spelled-out test. Will generalize to other fractional powers when c++26 available */ + { + constexpr auto p = power_ratio_type(-1, 2); + + constexpr auto orig_bpu = bpu(dim::time, + scalefactor_ratio_type(360*24*3600, 1), + power_ratio_type(-1, 2)); + static_assert(orig_bpu.native_dim() == dim::time); + + constexpr auto new_scalefactor = scalefactor_ratio_type(30*24*3600, 1); + + /* orig ~ 360d volatility, new = 30d volatility */ + constexpr auto mult = orig_bpu.scalefactor() / new_scalefactor; + log && log(xtag("mult", mult)); + static_assert(mult.num() == 12); + static_assert(mult.den() == 1); + + constexpr auto p_floor = orig_bpu.power().floor(); + static_assert(p_floor == 0); + + constexpr auto p_frac = orig_bpu.power().frac().template convert_to(); + static_assert(p_frac == -0.5); + + constexpr auto outer_sf_exact = mult.power(p_floor); + static_assert(outer_sf_exact.num() == 1); + static_assert(outer_sf_exact.den() == 1); + + constexpr auto mult_inexact = mult.template convert_to(); + static_assert(mult_inexact == 12.0); + + constexpr auto rr = bpu2_rescale(orig_bpu, scalefactor_ratio_type(30*24*3600, 1)); + + log && log(xtag("rr.outer_scale_exact", rr.outer_scale_factor_), + xtag("rr.outer_scale_sq", rr.outer_scale_sq_)); + + static_assert(rr.bpu_rescaled_.power() == power_ratio_type(-1,2)); + static_assert(rr.outer_scale_factor_ == outer_sf_exact); + static_assert(rr.outer_scale_sq_ == 1 / 12.0); + } + + /* keep spelled-out test. Will generalize to other fractional powers when c++26 available */ + { + constexpr auto p = power_ratio_type(-3, 2); + + constexpr auto orig_bpu = bpu(dim::time, + scalefactor_ratio_type(360*24*3600, 1), + power_ratio_type(-3, 2)); + static_assert(orig_bpu.native_dim() == dim::time); + + constexpr auto new_scalefactor = scalefactor_ratio_type(30*24*3600, 1); + + constexpr auto mult = orig_bpu.scalefactor() / new_scalefactor; + log && log(xtag("mult", mult)); + static_assert(mult.num() == 12); + static_assert(mult.den() == 1); + + constexpr auto p_floor = orig_bpu.power().floor(); + static_assert(p_floor == -1); + + constexpr auto p_frac = orig_bpu.power().frac().template convert_to(); + static_assert(p_frac == -0.5); + + constexpr auto outer_sf_exact = mult.power(p_floor); + static_assert(outer_sf_exact.num() == 1); + static_assert(outer_sf_exact.den() == 12); + + constexpr auto mult_inexact = mult.template convert_to(); + static_assert(mult_inexact == 12.0); + + constexpr auto rr = bpu2_rescale(orig_bpu, scalefactor_ratio_type(30*24*3600, 1)); + + log && log(xtag("rr.outer_scale_exact", rr.outer_scale_factor_), + xtag("rr.outer_scale_sq", rr.outer_scale_sq_)); + + static_assert(rr.bpu_rescaled_.power() == power_ratio_type(-3,2)); + static_assert(rr.outer_scale_factor_ == outer_sf_exact); + static_assert(rr.outer_scale_sq_ == 1 / 12.0); + } + } /*TEST_CASE(bpu_rescale)*/ + + TEST_CASE("bpu_product", "[bpu_product]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.bpu_product")); + //log && log("(A)", xtag("foo", foo)); + + { + constexpr auto bpu_x = bpu(dim::time, + scalefactor_ratio_type(360*24*3600, 1), + power_ratio_type(-3,2)); + static_assert(bpu_x.native_dim() == dim::time); + + constexpr auto bpu_y = bpu(dim::time, + scalefactor_ratio_type(360*24*3600, 1), + power_ratio_type(1,2)); + static_assert(bpu_y.native_dim() == dim::time); + +#ifdef NOT_USING + constexpr auto bpu_prod = bpu2_product(bpu_x, bpu_y); + + log && log(xtag("bpu_prod.bpu_rescaled", bpu_prod.bpu_rescaled_)); + log && log(xtag("bpu_prod.outer_scale_exact", bpu_prod.outer_scale_exact_)); + log && log(xtag("bpu_prod.outer_scale_sq", bpu_prod.outer_scale_sq_)); + + static_assert(bpu_prod.bpu_rescaled_.native_dim() == dim::time); + static_assert(bpu_prod.bpu_rescaled_.scalefactor() == scalefactor_ratio_type(360*24*3600, 1)); + static_assert(bpu_prod.bpu_rescaled_.power() == power_ratio_type(-1, 1)); + static_assert(bpu_prod.outer_scale_exact_ == scalefactor_ratio_type(1,1)); + static_assert(bpu_prod.outer_scale_sq_ == 1.0); +#endif + } + } /*TEST_CASE(bpu_product)*/ + + TEST_CASE("bpu_product2", "[bpu_product]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.bpu_product2")); + //log && log("(A)", xtag("foo", foo)); + + { + constexpr auto bpu_x = bpu(dim::time, + scalefactor_ratio_type(360*24*3600, 1), + power_ratio_type(-3,2)); + static_assert(bpu_x.native_dim() == dim::time); + + constexpr auto bpu_y = bpu(dim::time, + scalefactor_ratio_type(30*24*3600, 1), + power_ratio_type(1,2)); + static_assert(bpu_y.native_dim() == dim::time); + +#ifdef NOT_USING + constexpr auto bpu_prod = bpu2_product(bpu_x, bpu_y); + + log && log(xtag("bpu_prod.bpu_rescaled", bpu_prod.bpu_rescaled_)); + log && log(xtag("bpu_prod.outer_scale_exact", bpu_prod.outer_scale_exact_)); + log && log(xtag("bpu_prod.outer_scale_sq", bpu_prod.outer_scale_sq_)); + + static_assert(bpu_prod.bpu_rescaled_.native_dim() == dim::time); + static_assert(bpu_prod.bpu_rescaled_.scalefactor() == scalefactor_ratio_type(360*24*3600, 1)); + static_assert(bpu_prod.bpu_rescaled_.power() == power_ratio_type(-1, 1)); + static_assert(bpu_prod.outer_scale_exact_ == scalefactor_ratio_type(1,1)); + static_assert(bpu_prod.outer_scale_sq_ == 1.0/12.0); +#endif + } + } /*TEST_CASE(bpu_product2)*/ + + TEST_CASE("bpu_array", "[bpu_array]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.bpu_array")); + //log && log("(A)", xtag("foo", foo)); + + { + constexpr natural_unit v; + + static_assert(v.n_bpu() == 0); + } + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::mass, scalefactor_ratio_type(1000, 1), power_ratio_type(1, 1)))); + + static_assert(v.n_bpu() == 1); + } + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::distance, scalefactor_ratio_type(1, 1000), power_ratio_type(2, 1)), + bpu(dim::mass, scalefactor_ratio_type(1, 1000), power_ratio_type(-1, 1)))); + + static_assert(v.n_bpu() == 2); + } + } /*TEST_CASE(bpu_array)*/ + } /*namespace qty*/ +} /*namespace xo*/ + + +/* end natural_unit.test.cpp */ diff --git a/xo-unit/utest/quantity.test.cpp b/xo-unit/utest/quantity.test.cpp new file mode 100644 index 00000000..a02da256 --- /dev/null +++ b/xo-unit/utest/quantity.test.cpp @@ -0,0 +1,518 @@ +/* @file quantity.test.cpp */ + +#include "xo/unit/quantity.hpp" +#include "xo/unit/quantity_iostream.hpp" +#include "xo/unit/quantity_concept.hpp" +#include "xo/indentlog/scope.hpp" +#include + +namespace xo { + namespace qty { + TEST_CASE("quantity.mass", "[quantity]") { + constexpr bool c_debug_flag = true; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.quantity.mass")); + //log && log("(A)", xtag("foo", foo)); + + constexpr auto pg = qty::picograms(1.0); + static_assert(quantity_concept); + static_assert(sizeof(pg) == sizeof(double)); + static_assert(pg.scale() == 1.0); + static_assert(pg.abbrev() == flatstring("pg")); + + constexpr auto ng = qty::nanograms(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ng) == sizeof(double)); + static_assert(ng.scale() == 1.0); + static_assert(ng.abbrev() == flatstring("ng")); + + constexpr auto ug = qty::micrograms(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ug) == sizeof(double)); + static_assert(ug.scale() == 1.0); + static_assert(ug.abbrev() == flatstring("ug")); + + constexpr auto mg = qty::milligrams(1.0); + static_assert(quantity_concept); + static_assert(sizeof(mg) == sizeof(double)); + static_assert(mg.scale() == 1.0); + static_assert(mg.abbrev() == flatstring("mg")); + + constexpr auto g = qty::grams(1.0); + static_assert(quantity_concept); + static_assert(sizeof(g) == sizeof(double)); + static_assert(g.scale() == 1.0); + static_assert(g.abbrev() == flatstring("g")); + + constexpr auto kg = qty::kilograms(1.0); + static_assert(quantity_concept); + static_assert(sizeof(kg) == sizeof(double)); + static_assert(kg.scale() == 1.0); + static_assert(kg.abbrev() == flatstring("kg")); + + constexpr auto t = qty::tonnes(1.0); + static_assert(quantity_concept); + static_assert(sizeof(t) == sizeof(double)); + static_assert(t.scale() == 1.0); + static_assert(t.abbrev() == flatstring("t")); + + constexpr auto kt = qty::kilotonnes(1.0); + static_assert(quantity_concept); + static_assert(sizeof(kt) == sizeof(double)); + static_assert(kt.scale() == 1.0); + static_assert(kt.abbrev() == flatstring("kt")); + + constexpr auto mt = qty::megatonnes(1.0); + static_assert(quantity_concept); + static_assert(sizeof(mt) == sizeof(double)); + static_assert(mt.scale() == 1.0); + static_assert(mt.abbrev() == flatstring("Mt")); + + constexpr auto gt = qty::gigatonnes(1.0); + static_assert(quantity_concept); + static_assert(sizeof(gt) == sizeof(double)); + static_assert(gt.scale() == 1.0); + static_assert(gt.abbrev() == flatstring("Gt")); + + log && log(xtag("pg.abbrev", pg.abbrev())); + log && log(xtag("ng.abbrev", ng.abbrev())); + log && log(xtag("ug.abbrev", ug.abbrev())); + log && log(xtag("mg.abbrev", mg.abbrev())); + log && log(xtag("g.abbrev", g.abbrev())); + log && log(xtag("kg.abbrev", kg.abbrev())); + log && log(xtag("t.abbrev", t.abbrev())); + log && log(xtag("kt.abbrev", kt.abbrev())); + log && log(xtag("mt.abbrev", mt.abbrev())); + log && log(xtag("gt.abbrev", gt.abbrev())); + + log && log(xtag("pg", pg)); + + REQUIRE(tostr(pg) == "1pg"); + REQUIRE(tostr(ng) == "1ng"); + REQUIRE(tostr(ug) == "1ug"); + REQUIRE(tostr(mg) == "1mg"); + REQUIRE(tostr(g) == "1g"); + REQUIRE(tostr(kg) == "1kg"); + REQUIRE(tostr(t) == "1t"); + REQUIRE(tostr(kt) == "1kt"); + REQUIRE(tostr(mt) == "1Mt"); + REQUIRE(tostr(gt) == "1Gt"); + } /*TEST_CASE(quantity.mass)*/ + + TEST_CASE("quantity.distance", "[quantity]") { + constexpr bool c_debug_flag = true; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.quantity.distance")); + + constexpr auto pm = qty::picometers(1.0); + static_assert(quantity_concept); + static_assert(sizeof(pm) == sizeof(double)); + static_assert(pm.scale() == 1.0); + static_assert(pm.abbrev() == flatstring("pm")); + + constexpr auto nm = qty::nanometers(1.0); + static_assert(quantity_concept); + static_assert(sizeof(nm) == sizeof(double)); + static_assert(nm.scale() == 1.0); + static_assert(nm.abbrev() == flatstring("nm")); + + constexpr auto um = qty::micrometers(1.0); + static_assert(quantity_concept); + static_assert(sizeof(um) == sizeof(double)); + static_assert(um.scale() == 1.0); + static_assert(um.abbrev() == flatstring("um")); + + constexpr auto mm = qty::millimeters(1.0); + static_assert(quantity_concept); + static_assert(sizeof(mm) == sizeof(double)); + static_assert(mm.scale() == 1.0); + static_assert(mm.abbrev() == flatstring("mm")); + + constexpr auto m = qty::meters(1.0); + static_assert(quantity_concept); + static_assert(sizeof(m) == sizeof(double)); + static_assert(m.scale() == 1.0); + static_assert(m.abbrev() == flatstring("m")); + + constexpr auto km = qty::kilometers(1.0); + static_assert(quantity_concept); + static_assert(sizeof(km) == sizeof(double)); + static_assert(km.scale() == 1.0); + static_assert(km.abbrev() == flatstring("km")); + + constexpr auto Mm = qty::megameters(1.0); + static_assert(quantity_concept); + static_assert(sizeof(Mm) == sizeof(double)); + static_assert(Mm.scale() == 1.0); + static_assert(Mm.abbrev() == flatstring("Mm")); + + constexpr auto Gm = qty::gigameters(1.0); + static_assert(quantity_concept); + static_assert(sizeof(Gm) == sizeof(double)); + static_assert(Gm.scale() == 1.0); + static_assert(Gm.abbrev() == flatstring("Gm")); + + constexpr auto lsec = qty::lightseconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(lsec) == sizeof(double)); + static_assert(lsec.scale() == 1.0); + static_assert(lsec.abbrev() == flatstring("lsec")); + + constexpr auto AU = qty::astronomicalunits(1.0); + static_assert(quantity_concept); + static_assert(sizeof(AU) == sizeof(double)); + static_assert(AU.scale() == 1.0); + static_assert(AU.abbrev() == flatstring("AU")); + + log && log(xtag("pm.abbrev", pm.abbrev())); + log && log(xtag("nm.abbrev", nm.abbrev())); + log && log(xtag("um.abbrev", um.abbrev())); + log && log(xtag("mm.abbrev", mm.abbrev())); + log && log(xtag("m.abbrev", m.abbrev())); + log && log(xtag("km.abbrev", km.abbrev())); + log && log(xtag("Mm.abbrev", Mm.abbrev())); + log && log(xtag("Gm.abbrev", Gm.abbrev())); + log && log(xtag("lsec.abbrev", lsec.abbrev())); + log && log(xtag("AU.abbrev", AU.abbrev())); + + REQUIRE(tostr(pm) == "1pm"); + REQUIRE(tostr(nm) == "1nm"); + REQUIRE(tostr(um) == "1um"); + REQUIRE(tostr(mm) == "1mm"); + REQUIRE(tostr(m) == "1m"); + REQUIRE(tostr(km) == "1km"); + REQUIRE(tostr(Mm) == "1Mm"); + REQUIRE(tostr(Gm) == "1Gm"); + REQUIRE(tostr(lsec) == "1lsec"); + REQUIRE(tostr(AU) == "1AU"); + + } /*TEST_CASE(quantity.distance)*/ + + TEST_CASE("quantity.time", "[quantity]") { + constexpr bool c_debug_flag = true; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.quantity.time")); + + constexpr auto ps = qty::picoseconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ps) == sizeof(double)); + static_assert(ps.scale() == 1.0); + static_assert(ps.abbrev() == flatstring("ps")); + + constexpr auto ns = qty::nanoseconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ns) == sizeof(double)); + static_assert(ns.scale() == 1.0); + static_assert(ns.abbrev() == flatstring("ns")); + + constexpr auto us = qty::microseconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(us) == sizeof(double)); + static_assert(us.scale() == 1.0); + static_assert(us.abbrev() == flatstring("us")); + + constexpr auto ms = qty::milliseconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ms) == sizeof(double)); + static_assert(ms.scale() == 1.0); + static_assert(ms.abbrev() == flatstring("ms")); + + constexpr auto s = qty::seconds(1.0); + static_assert(quantity_concept); + static_assert(sizeof(s) == sizeof(double)); + static_assert(s.scale() == 1.0); + static_assert(s.abbrev() == flatstring("s")); + + constexpr auto min = qty::minutes(1.0); + static_assert(quantity_concept); + static_assert(sizeof(min) == sizeof(double)); + static_assert(min.scale() == 1.0); + static_assert(min.abbrev() == flatstring("min")); + + constexpr auto hr = qty::hours(1.0); + static_assert(quantity_concept); + static_assert(sizeof(hr) == sizeof(double)); + static_assert(hr.scale() == 1.0); + static_assert(hr.abbrev() == flatstring("hr")); + + constexpr auto dy = qty::days(1.0); + static_assert(quantity_concept); + static_assert(sizeof(dy) == sizeof(double)); + static_assert(dy.scale() == 1.0); + static_assert(dy.abbrev() == flatstring("dy")); + + constexpr auto wk = qty::weeks(1.0); + static_assert(quantity_concept); + static_assert(sizeof(wk) == sizeof(double)); + static_assert(wk.scale() == 1.0); + static_assert(wk.abbrev() == flatstring("wk")); + + constexpr auto mo = qty::months(1.0); + static_assert(quantity_concept); + static_assert(sizeof(mo) == sizeof(double)); + static_assert(mo.scale() == 1.0); + static_assert(mo.abbrev() == flatstring("mo")); + + constexpr auto yr = qty::years(1.0); + static_assert(quantity_concept); + static_assert(sizeof(yr) == sizeof(double)); + static_assert(yr.scale() == 1.0); + static_assert(yr.abbrev() == flatstring("yr")); + + constexpr auto yr250 = qty::year250s(1.0); + static_assert(quantity_concept); + static_assert(sizeof(yr250) == sizeof(double)); + static_assert(yr250.scale() == 1.0); + static_assert(yr250.abbrev() == flatstring("yr250")); + + constexpr auto yr360 = qty::year360s(1.0); + static_assert(quantity_concept); + static_assert(sizeof(yr360) == sizeof(double)); + static_assert(yr360.scale() == 1.0); + static_assert(yr360.abbrev() == flatstring("yr360")); + + constexpr auto yr365 = qty::year365s(1.0); + static_assert(quantity_concept); + static_assert(sizeof(yr365) == sizeof(double)); + static_assert(yr365.scale() == 1.0); + static_assert(yr365.abbrev() == flatstring("yr365")); + + log && log(xtag("ps.abbrev", ps.abbrev())); + log && log(xtag("ns.abbrev", ns.abbrev())); + log && log(xtag("us.abbrev", us.abbrev())); + log && log(xtag("ms.abbrev", ms.abbrev())); + log && log(xtag("s.abbrev", s.abbrev())); + log && log(xtag("min.abbrev", min.abbrev())); + log && log(xtag("hr.abbrev", hr.abbrev())); + log && log(xtag("dy.abbrev", dy.abbrev())); + log && log(xtag("wk.abbrev", wk.abbrev())); + log && log(xtag("mo.abbrev", mo.abbrev())); + log && log(xtag("yr.abbrev", yr.abbrev())); + log && log(xtag("yr250.abbrev", yr250.abbrev())); + log && log(xtag("yr360.abbrev", yr360.abbrev())); + log && log(xtag("yr365.abbrev", yr365.abbrev())); + + REQUIRE(tostr(ps) == "1ps"); + REQUIRE(tostr(ns) == "1ns"); + REQUIRE(tostr(us) == "1us"); + REQUIRE(tostr(ms) == "1ms"); + REQUIRE(tostr(s) == "1s"); + REQUIRE(tostr(min) == "1min"); + REQUIRE(tostr(hr) == "1hr"); + REQUIRE(tostr(dy) == "1dy"); + REQUIRE(tostr(wk) == "1wk"); + REQUIRE(tostr(mo) == "1mo"); + REQUIRE(tostr(yr) == "1yr"); + REQUIRE(tostr(yr250) == "1yr250"); + REQUIRE(tostr(yr360) == "1yr360"); + REQUIRE(tostr(yr365) == "1yr365"); + } /*TEST_CASE(quantity.time)*/ + + TEST_CASE("quantity.mult", "[quantity.mult]") { + constexpr auto pg = qty::picograms(1.0); + constexpr auto ng = qty::nanograms(1.0); + constexpr auto ug = qty::micrograms(1.0); + + constexpr auto ng_in_pg = ng.rescale_ext(); + + static_assert(ng_in_pg.scale() == 1000); + static_assert(ng_in_pg == pg * 1000); + + /* multiplication by dimensionless values is constexpr in c++23 + * comparison in not constexpr until c++26 + */ + + /* picograms:nanograms */ + + static_assert(pg * 1000 == ng); + REQUIRE(pg * 1000 == ng); + static_assert(1000 * pg == ng); + REQUIRE(1000 * pg == ng); + static_assert(ng * 0.001 == pg); + REQUIRE(ng * 0.001 == pg); + static_assert(0.001 * ng == pg); + REQUIRE(0.001 * ng == pg); + + /* picograms:micrograms */ + + static_assert(pg * 1e6 == ug); + REQUIRE(pg * 1e6 == ug); + static_assert(1e6 * pg == ug); + REQUIRE(1e6 * pg == ug); + static_assert(ug * 1e-6 == pg); + REQUIRE(ug * 1e-6 == pg); + static_assert(1e-6 * ug == pg); + REQUIRE(1e-6 * ug == pg); + + /* nanograms:micrograms */ + + static_assert(ng * 1e3 == ug); + REQUIRE(ng * 1e3 == ug); + static_assert(1e3 * ng == ug); + REQUIRE(1e3 * ng == ug); + static_assert(ug * 1e-3 == ng); + REQUIRE(ug * 1e-3 == ng); + static_assert(1e-3 * ug == ng); + REQUIRE(1e-3 * ug == ng); + + // /* picograms:milligrams */ + // /* nanograms:milligrams */ + // /* micrograms:milligrams */ + + // /* picograms:grams */ + // /* nanograms:grams */ + // /* micrograms:grams */ + // /* milligrams:grams */ + } /*TEST_CASE(quantity.mult)*/ + + TEST_CASE("quantity.currency", "[quantity.currency]") { + constexpr bool c_debug_flag = true; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.quantity.time")); + + constexpr auto ccy = qty::currency(1.0); + static_assert(quantity_concept); + static_assert(sizeof(ccy) == sizeof(double)); + static_assert(ccy.scale() == 1.0); + static_assert(ccy.abbrev() == flatstring("ccy")); + + log && log(xtag("ccy.abbrev", ccy.abbrev())); + REQUIRE(tostr(ccy) == "1ccy"); + } /*TEST_CASE(quantity.currency)*/ + + TEST_CASE("quantity.mult2", "[quantity.mult]") { + constexpr auto ms = qty::milliseconds(1.0); + + /* proof that ms.s_unit is constexpr */ + static_assert(ms.s_scaled_unit.n_bpu() == 1); + + /* proof that ms.unit() is constexpr */ + static_assert(ms.unit().n_bpu() == 1); + + constexpr auto rr = detail::su_product(ms.unit().natural_unit_, + ms.unit().natural_unit_); + + /* proof that detail::su_product<..>(..) return value is constexpr */ + static_assert(rr.outer_scale_sq_ == 1.0); + static_assert(rr.outer_scale_factor_.template convert_to() == 1.0); + static_assert(rr.natural_unit_.n_bpu() == 1); + + constexpr auto q1 = quantity(rr.natural_unit_), + decltype(ms)::repr_type>(ms.scale() * ms.scale()); + + /* proof that q is constexpr */ + static_assert(q1.scale() == 1.0); + static_assert(q1.unit().n_bpu() == 1); + + { + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + constexpr auto rr = detail::su_product(ms.unit().natural_unit_, + ms.unit().natural_unit_); + + static_assert(rr.outer_scale_sq_ == 1.0); + } + + constexpr auto q2 = detail::quantity_util::multiply(ms, ms); + + /* proof that q2 is constexpr */ + static_assert(q2.scale() == 1.0); + static_assert(q2.unit().n_bpu() == 1); + static_assert(q2.unit()[0].power() == power_ratio_type(2,1)); + + constexpr auto q3 = ms * ms; + + /* proof that q3 is constexpr */ + static_assert(q3.scale() == 1.0); + static_assert(q3.unit().n_bpu() == 1); + static_assert(q3.unit()[0].power() == power_ratio_type(2,1)); + + } /*TEST_CASE(quantity.mult2)*/ + + TEST_CASE("quantity.mult3", "[quantity.mult]") { + constexpr auto ng = qty::nanogram; + + constexpr auto l_qty = 7.55 * ng; + constexpr auto r_qty = ng * 7.55; + + static_assert(l_qty.unit().n_bpu() == 1); + static_assert(l_qty.unit()[0].native_dim() == dim::mass); + static_assert(l_qty.unit()[0].power() == xo::ratio::ratio(1,1)); + static_assert(l_qty.unit()[0].scalefactor() == xo::ratio::ratio(1,1000000000)); + static_assert(l_qty.scale() == 7.55); + + static_assert(r_qty.unit().n_bpu() == 1); + static_assert(r_qty.unit()[0].native_dim() == dim::mass); + static_assert(r_qty.unit()[0].power() == xo::ratio::ratio(1,1)); + static_assert(r_qty.unit()[0].scalefactor() == xo::ratio::ratio(1,1000000000)); + static_assert(r_qty.scale() == 7.55); + } + + TEST_CASE("quantity.div2", "[quantity.div]") { + constexpr auto ms = qty::milliseconds(1.0); + + /* proof that ms.s_unit is constexpr */ + static_assert(ms.s_scaled_unit.n_bpu() == 1); + + /* proof that ms.unit() is constexpr */ + static_assert(ms.unit().n_bpu() == 1); + + constexpr auto rr = detail::su_ratio(ms.unit().natural_unit_, + ms.unit().natural_unit_); + + /* proof that detail::su_product<..>(..) return value is constexpr */ + static_assert(rr.outer_scale_sq_ == 1.0); + static_assert(rr.outer_scale_factor_.template convert_to() == 1.0); + static_assert(rr.natural_unit_.n_bpu() == 0); + + constexpr auto q1 = quantity(rr.natural_unit_), + decltype(ms)::repr_type>(ms.scale() * ms.scale()); + + /* proof that q is constexpr */ + static_assert(q1.scale() == 1.0); + static_assert(q1.unit().n_bpu() == 0); + + { + using r_int_type = std::common_type_t; + using r_int2x_type = std::common_type_t; + + constexpr auto rr = detail::su_ratio(ms.unit().natural_unit_, + ms.unit().natural_unit_); + + static_assert(rr.outer_scale_sq_ == 1.0); + } + + constexpr auto q2 = detail::quantity_util::divide(ms, ms); + + /* proof that q2 is constexpr */ + static_assert(q2.is_dimensionless()); + static_assert(q2.scale() == 1.0); + static_assert(q2.unit().n_bpu() == 0); + static_assert(q2.unit()[0].power() == power_ratio_type(0,1)); + + constexpr auto q3 = ms / ms; + + /* proof that q3 is constexpr */ + static_assert(q3.is_dimensionless()); + static_assert(q3.scale() == 1.0); + static_assert(q3.unit().n_bpu() == 0); + static_assert(q3.unit()[0].power() == power_ratio_type(0,1)); + + } /*TEST_CASE(quantity.mult2)*/ + } /*namespace qty*/ +} /*namespace xo*/ + +/* end quantity.test.cpp */ diff --git a/xo-unit/utest/scaled_unit.test.cpp b/xo-unit/utest/scaled_unit.test.cpp new file mode 100644 index 00000000..408e43ab --- /dev/null +++ b/xo-unit/utest/scaled_unit.test.cpp @@ -0,0 +1,121 @@ +/* @file scaled_unit.test.cpp */ + +#include "xo/unit/scaled_unit.hpp" +#include "xo/unit/scaled_unit_iostream.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + namespace qty { + using su64_type = scaled_unit; + using xo::qty::detail::su_product; + using xo::qty::detail::nu_maker; + + /* compile-time test: + * verify we can use an su64_type instance as a non-type template parameter. + * Will need this for quantity> + */ + template + constexpr su64_type su_reciprocal = su.reciprocal(); + + TEST_CASE("scaled_unit", "[scaled_unit]") { + //static_assert(u::gram.reciprocal().reciprocal() == u::gram); + //REQUIRE(u::gram.reciporcal().reciprocal() == u::gram); + + static_assert(u::gram.reciprocal().outer_scale_factor_ == 1); + REQUIRE(u::gram.reciprocal().outer_scale_factor_ == 1); + + static_assert(u::gram.reciprocal().outer_scale_sq_ == 1.0); + REQUIRE(u::gram.reciprocal().outer_scale_sq_ == 1.0); + } /*TEST_CASE(scaled_unit)*/ + + TEST_CASE("su_product", "[scaled_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.su_product")); + + { + constexpr natural_unit v + = (nu_maker::make_nu + (bpu(dim::distance, scalefactor_ratio_type(1, 1000), power_ratio_type(2, 1)) /*mm^2*/, + bpu(dim::mass, scalefactor_ratio_type(1, 1000), power_ratio_type(-1, 1)) /*mg^-1*/)); + + static_assert(v.n_bpu() == 2); + + constexpr natural_unit w + = (nu_maker::make_nu + (bpu(dim::time, scalefactor_ratio_type(30*24*3600, 1), power_ratio_type(-1, 2)))); + + static_assert(w.n_bpu() == 1); + + constexpr auto prod_rr = su_product(v, w); + + log && log(xtag("prod_rr.bpu_array", prod_rr.natural_unit_)); + log && log(xtag("prod_rr.outer_scale_exact", prod_rr.outer_scale_factor_.convert_to())); + log && log(xtag("prod_rr.outer_scale_sq", prod_rr.outer_scale_sq_)); + + static_assert(prod_rr.natural_unit_.n_bpu() == 3); + static_assert(prod_rr.natural_unit_[0].native_dim() == dim::distance); + static_assert(prod_rr.natural_unit_[0].scalefactor() == scalefactor_ratio_type(1, 1000)); + static_assert(prod_rr.natural_unit_[0].power() == power_ratio_type(2, 1)); + static_assert(prod_rr.natural_unit_[1].native_dim() == dim::mass); + static_assert(prod_rr.natural_unit_[1].scalefactor() == scalefactor_ratio_type(1, 1000)); + static_assert(prod_rr.natural_unit_[1].power() == power_ratio_type(-1, 1)); + static_assert(prod_rr.natural_unit_[2].native_dim() == dim::time); + static_assert(prod_rr.natural_unit_[2].scalefactor() == scalefactor_ratio_type(30*24*3600, 1)); + static_assert(prod_rr.natural_unit_[2].power() == power_ratio_type(-1, 2)); + static_assert(prod_rr.outer_scale_factor_ == scalefactor_ratio_type(1, 1)); + static_assert(prod_rr.outer_scale_sq_ == 1.0); + } + } /*TEST_CASE(su_product)*/ + + TEST_CASE("scaled_unit0", "[scaled_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.scaled_unit0")); + + constexpr auto ng = u::nanogram; + constexpr auto ng2 = ng * ng; + + log && log(xtag("ng", ng)); + log && log(xtag("ng*ng", ng2)); + + static_assert(ng.natural_unit_.n_bpu() == 1); + static_assert(ng2.natural_unit_.n_bpu() == 1); + } /*TEST_CASE(scaled_unit0)*/ + + TEST_CASE("scaled_unit1", "[scaled_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.scaled_unit1")); + + constexpr auto ng = u::nanogram; + constexpr auto ng2 = ng / ng; + + log && log(xtag("ng", ng)); + log && log(xtag("ng/ng", ng2)); + + static_assert(ng.natural_unit_.n_bpu() == 1); + static_assert(ng2.natural_unit_.n_bpu() == 0); + } /*TEST_CASE(scaled_unit1)*/ + + TEST_CASE("scaled_unit2", "[scaled_unit]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.scaled_unit2")); + + constexpr auto ms = u::millisecond; + constexpr auto ms2 = ms * ms; + + log && log(xtag("ms", ms)); + log && log(xtag("ms*ms", ms2)); + + static_assert(ms.natural_unit_.n_bpu() == 1); + static_assert(ms2.natural_unit_.n_bpu() == 1); + } /*TEST_CASE(scaled_unit2)*/ + + } /*namespace qty*/ +} /*namespace xo*/ + +/* end scaled_unit.test.cpp */ diff --git a/xo-unit/utest/unit.test.cpp b/xo-unit/utest/unit.test.cpp new file mode 100644 index 00000000..5ae8d3cc --- /dev/null +++ b/xo-unit/utest/unit.test.cpp @@ -0,0 +1,89 @@ +/* @file unit.test.cpp */ + +#include "xo/unit/scaled_unit_iostream.hpp" +//#include "xo/unit/scaled_unit.hpp" +#include "xo/unit/bu_store.hpp" +#include "xo/unit/bpu.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include + +namespace xo { + namespace ut { + /* compile-time tests */ + + namespace bu = xo::qty::detail::bu; + using xo::qty::bu_abbrev_store; + using xo::qty::bu_abbrev_type; + using xo::qty::scalefactor_ratio_type; + using xo::qty::power_ratio_type; + using xo::qty::dim; + + TEST_CASE("basis_unit2_store", "[basis_unit2_store]") { + constexpr bool c_debug_flag = false; + + // can get bits from /dev/random by uncommenting the 2nd line below + //uint64_t seed = xxx; + //rng::Seed seed; + + //auto rng = xo::rng::xoshiro256ss(seed); + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.basis_unit2_store")); + //log && log("(A)", xtag("foo", foo)); + + log && log(xtag("mass*10^-9", bu_abbrev_store.bu_abbrev(bu::picogram))); + log && log(xtag("mass*10^-6", bu_abbrev_store.bu_abbrev(bu::microgram))); + log && log(xtag("mass*10^-3", bu_abbrev_store.bu_abbrev(bu::milligram))); + log && log(xtag("mass", bu_abbrev_store.bu_abbrev(bu::gram))); + log && log(xtag("mass*10^3", bu_abbrev_store.bu_abbrev(bu::kilogram))); + log && log(xtag("mass*10^6", bu_abbrev_store.bu_abbrev(bu::tonne))); + log && log(xtag("mass*10^9", bu_abbrev_store.bu_abbrev(bu::megatonne))); + + log && log(xtag("distance*10^-9", bu_abbrev_store.bu_abbrev(bu::nanometer))); + log && log(xtag("distance*10^-6", bu_abbrev_store.bu_abbrev(bu::micrometer))); + log && log(xtag("distance*10^-3", bu_abbrev_store.bu_abbrev(bu::millimeter))); + log && log(xtag("distance", bu_abbrev_store.bu_abbrev(bu::meter))); + log && log(xtag("distance*10^3", bu_abbrev_store.bu_abbrev(bu::kilometer))); + + log && log(xtag("time*10^-9", bu_abbrev_store.bu_abbrev(bu::nanosecond))); + log && log(xtag("time*10^-6", bu_abbrev_store.bu_abbrev(bu::microsecond))); + log && log(xtag("time*10^-3", bu_abbrev_store.bu_abbrev(bu::millisecond))); + log && log(xtag("time", bu_abbrev_store.bu_abbrev(bu::second))); + log && log(xtag("time*60", bu_abbrev_store.bu_abbrev(bu::minute))); + log && log(xtag("time*3600", bu_abbrev_store.bu_abbrev(bu::hour))); + log && log(xtag("time*24*3600", bu_abbrev_store.bu_abbrev(bu::day))); + log && log(xtag("time*250*24*3600", bu_abbrev_store.bu_abbrev(bu::year250))); + log && log(xtag("time*360*24*3600", bu_abbrev_store.bu_abbrev(bu::year360))); + log && log(xtag("time*365*24*3600", bu_abbrev_store.bu_abbrev(bu::year365))); + + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::nanogram), "ng") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::microgram), "ug") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::milligram), "mg") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::gram), "g") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::kilogram), "kg") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::tonne), "t") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::kilotonne), "kt") == 0); + + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::nanometer), "nm") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::micrometer), "um") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::millimeter), "mm") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::meter), "m") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::kilometer), "km") == 0); + + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::nanosecond), "ns") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::microsecond), "us") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::millisecond), "ms") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::second), "s") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::minute), "min") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::hour), "hr") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::day), "dy") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::year250), "yr250") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::year360), "yr360") == 0); + REQUIRE(::strcmp(bu_abbrev_store.bu_abbrev(bu::year365), "yr365") == 0); + + } /*TEST_CASE(basis_unit2_store)*/ + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end dimension.test.cpp */ diff --git a/xo-unit/utest/unit_utest_main.cpp b/xo-unit/utest/unit_utest_main.cpp new file mode 100644 index 00000000..81f2ade0 --- /dev/null +++ b/xo-unit/utest/unit_utest_main.cpp @@ -0,0 +1,6 @@ +/* file unit_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end unit_utest_main.cpp */ diff --git a/xo-unit/utest/xquantity.test.cpp b/xo-unit/utest/xquantity.test.cpp new file mode 100644 index 00000000..5219fca4 --- /dev/null +++ b/xo-unit/utest/xquantity.test.cpp @@ -0,0 +1,676 @@ +/* @file xquantity.test.cpp */ + +#include "xquantity.hpp" +#include "xquantity_iostream.hpp" +#include "xo/randomgen/random_seed.hpp" +#include "xo/randomgen/xoshiro256.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include +#include +#include +#include + +namespace xo { + namespace u = xo::qty::u; + namespace nu = xo::qty::nu; + + using xo::qty::xquantity; + using xo::qty::natural_unit; + using xo::qty::power_ratio_type; + using xo::qty::scalefactor_ratio_type; + using xo::qty::dim; + using xo::qty::n_dim; + + using xo::rng::xoshiro256ss; + + using std::vector; + using std::int64_t; + using std::size_t; + + namespace ut { + std::string + int128_to_string(__int128_t x) { + size_t p = 256; + char buf[256]; + + buf[--p] = '\0'; + + bool minus_flag = (x < 0); + + if (minus_flag) + x = -x; + + while (p > 1) { + if (x == 0) + break; + + __int128_t x1 = x/10; + + auto digit = (x - 10*x1); /* not sure if % works on __int128_t */ + + buf[--p] = '0' + digit; + + x = x1; + } + + if (minus_flag) + buf[--p] = '-'; + + return std::string(buf + p); + } + +#ifdef NOT_USING + /* use Int2x to accumulate scalefactor */ + template + scaled_unit + nu_ratio_debug(const natural_unit & nu_lhs, + const natural_unit & nu_rhs) + { + XO_SCOPE(log, always); + + natural_unit ratio = nu_lhs.template to_repr(); + + /* accumulate product of scalefactors spun off by rescaling + * any basis-units in rhs_bpu_array that conflict with the same dimension + * in lh_bpu_array + */ + auto sfr = (xo::qty::detail::outer_scalefactor_result + (ratio::ratio(1, 1) /*outer_scale_exact*/, + 1.0 /*outer_scale_sq*/)); + + for (std::size_t i = 0; i < nu_rhs.n_bpu(); ++i) { + log && log(xtag("i", i)); + log && log(xtag("ratio[before]", ratio)); + + auto sfr2 = xo::qty::detail::nu_ratio_inplace(&ratio, nu_rhs[i].template to_repr()); + + log && log(xtag("nu_rhs[i]", nu_rhs[i])); + log && log(xtag("sfr2.outer_scale_exact.num", int128_to_string(sfr2.outer_scale_exact_.num()))); + log && log(xtag("sfr2.outer_scale_exact.den", int128_to_string(sfr2.outer_scale_exact_.den()))); + log && log(xtag("sfr2.outer_scale_sq", sfr2.outer_scale_sq_)); + + /* note: nu_ratio_inplace() reports multiplicative outer scaling factors, + * so multiply is correct here + */ + sfr.outer_scale_exact_ = sfr.outer_scale_exact_ * sfr2.outer_scale_exact_; + sfr.outer_scale_sq_ *= sfr2.outer_scale_sq_; + + log && log(xtag("sfr.outer_scale_exact.num", int128_to_string(sfr.outer_scale_exact_.num()))); + log && log(xtag("sfr.outer_scale_exact.den", int128_to_string(sfr.outer_scale_exact_.den()))); + } + + log && log(xtag("ratio[after]", ratio)); + + return scaled_unit(ratio.template to_repr(), + sfr.outer_scale_exact_, + sfr.outer_scale_sq_); + } +#endif + + vector> mass_unit_v + = { nu::picogram, nu::nanogram, nu::microgram, nu::milligram, + nu::gram, + nu::kilogram, nu::tonne, nu::kilotonne, nu::megatonne }; + + vector> distance_unit_v + = { nu::picometer, nu::nanometer, nu::micrometer, nu::millimeter, nu::meter, + nu::kilometer, nu::megameter, nu::gigameter, nu::lightsecond, nu::astronomicalunit }; + + vector> time_unit_v + = { nu::picosecond, nu::nanosecond, nu::microsecond, nu::millisecond, + nu::second, nu::minute, nu::hour, nu::day, nu::week, nu::month, nu::year, + nu::year250, nu::year360, nu::year365 }; + + vector> currency_unit_v + = { nu::currency }; + + vector> price_unit_v + = { nu::price }; + + vector> *> all_unit_v = { + &mass_unit_v, &distance_unit_v, &time_unit_v, ¤cy_unit_v, &price_unit_v + }; + + template + void + quantity_tests(bool debug_flag, Rng & rng) + { + REQUIRE(all_unit_v.size() == n_dim); + + /* max number of basis_units to combine. don't combine a unit more than once + * (because can have too-extreme scaling differences) + */ + std::size_t n_bu = 5; + /* number of combinations to consider within each number up to n_bu */ + std::size_t n_experiment = 10; + + for (size_t nu=1; nu<=n_bu; ++nu) { + /* will combine nu basis units */ + + for (size_t i=0; i dim_set; + + for (size_t j=0; j(rng() % nu));; + + /* construct a pair of random product units with the same dimension; + * track relative scale as we go + */ + + xquantity q1 = natural_unit_qty(nu::dimensionless); + xquantity q2 = natural_unit_qty(nu::dimensionless); + + static_assert(std::same_as); + static_assert(std::same_as); + + double k1 = 0.0; /*q1/q2*/ + double k2 = 0.0; /*q2/q1*/ + { + xquantity q12 = (q1/q2); + xquantity q21 = (q2/q1); + + REQUIRE(q12.is_dimensionless()); + REQUIRE(q21.is_dimensionless()); + + k2 = q12.scale(); + k1 = q21.scale(); + } + + REQUIRE(k1 == 1.0); + REQUIRE(k2 == 1.0); + + /* inv: + * - q2 = q1*k1 + * - q2*k2 = q1 + */ + + /* Editorial: it's easy to produce units for which scaling requires working + * with rationals that have >128bits (ask me how I know). + * + * e.g. kilotonnes / nanograms is already 10^18 + * + * and 2^128 = (2^12)^10 * 2^8 ~ (10^3)^10 * 256 ~ 10^32 + * + * In below we cap magnitude differences at this much per basis unit + * Actual cap for q1/q2 is n_bu * max_magnitude + */ + constexpr double max_magdiff_per_bu = 1.1e5; + + for (xo::qty::dim d : dim_set) { + scope log(XO_DEBUG(debug_flag)); + + size_t d_j = static_cast(d); + + const auto * p_nu_v = all_unit_v[d_j]; + /* pick a random unit for selected dimension */ + auto nu1_j = (*p_nu_v)[rng() % p_nu_v->size()]; + + REQUIRE(nu1_j.n_bpu() == 1); + + int power = 1 + (rng() % 2); /* power in {1, 2} */ + if (rng() % 2) + power = -power; /* power in +/- {1,2} */ + + if (power == -1) + nu1_j = nu1_j.reciprocal(); + + xquantity q1_j = natural_unit_qty(nu1_j); + xquantity q2_j = q1_j; + xquantity r1; + xquantity r2; + + auto nu2_j = nu1_j; + auto nu2_j_ix = rng() % p_nu_v->size(); + + for (;;) { + REQUIRE(nu2_j_ix < p_nu_v->size()); + + nu2_j = (*p_nu_v)[nu2_j_ix]; + + if (power == -1) + nu2_j = nu2_j.reciprocal(); + + REQUIRE(nu2_j.n_bpu() == 1); + + double rx = (nu1_j[0].scalefactor().template convert_to() + / nu2_j[0].scalefactor().template convert_to()); + + if ((rx > max_magdiff_per_bu) || (rx < 1.0/max_magdiff_per_bu)) { + log && log(xtag("nu_z", p_nu_v->size()), xtag("nu2_j_ix", nu2_j_ix)); + log && log(xtag("nu1_j", nu1_j)); + log && log(xtag("nu2_j", nu2_j)); + log && log("rejecting ", xtag("rx", rx)); + + /* try another value for nu2_j */ + if (rx > max_magdiff_per_bu) { + /* try a larger value for nu2_j_ix */ + ++nu2_j_ix; + } else { + /* try a smaller value for nu2_j_ix */ + --nu2_j_ix; + } + + continue; + } + + q2_j = natural_unit_qty(nu2_j); + + r1 = q1_j / q2_j; + r2 = q2_j / q1_j; + + REQUIRE(r1.is_dimensionless()); + REQUIRE(r2.is_dimensionless()); + + break; + } + + q1 *= q1_j; + q2 *= q2_j; + + k1 *= r1.scale(); + k2 *= r2.scale(); + + log && log(xtag("d", xo::qty::dim2str(d))); + log && log(xtag("nu1_j", nu1_j)); + log && log(xtag("nu2_j", nu2_j)); + log && log(xtag("r1=q1_j/q2_j", r1.scale())); + log && log(xtag("r2=q2_j/q1_j", r2.scale())); + log && log(xtag("k1", k1)); + log && log(xtag("k2", k2)); + log && log(xtag("q1", q1)); + log && log(xtag("q2", q2)); + } + + INFO(xtag("k1=q1/q2", k1)); + INFO(XTAG(q1)); + INFO(xtag("k2=q2/q1", k2)); + INFO(XTAG(q2)); + + /* q1/q2, with exact representation (given no fractional dimensions) + * + */ + auto su = xo::qty::detail::su_ratio + (q1.unit(), q2.unit()); + + INFO(xtag("su.natural_unit", su.natural_unit_)); + INFO(xtag("su.outer_scale_exact", su.outer_scale_factor_)); + INFO(xtag("su.outer_scale_sq", su.outer_scale_sq_)); + + REQUIRE(q1 == q1); + REQUIRE(q2 == q2); + REQUIRE(su.natural_unit_.is_dimensionless()); + REQUIRE(su.outer_scale_sq_ == 1.0); + + /* these will only approximately be true in general */ + REQUIRE((q1/q2).scale() == Approx(k1).epsilon(1.0e-6)); + REQUIRE((q2/q1).scale() == Approx(k2).epsilon(1.0e-6)); + + if (abs(k1 - 1.0) > 1.0e-6) { + REQUIRE(q1 != q2); + REQUIRE(q2 != q1); + + if (k1 > 1.0) { + REQUIRE(q1 >= q2); + REQUIRE(q1 > q2); + + REQUIRE(q2 < q1); + REQUIRE(q2 <= q1); + } else { + REQUIRE(q1 <= q2); + REQUIRE(q1 < q2); + + REQUIRE(q2 > q1); + REQUIRE(q2 >= q1); + } + } + + auto q1_plus_q2 = q1 + q2; + /* k1 = q1/q2; k2 = q2/q1; so q1+q2 = q1(1 + q2/q1) = q1(1 + k2) */ + REQUIRE(q1_plus_q2.unit() == q1.unit()); + REQUIRE(q1_plus_q2.scale() == Approx(1.0 + k2).epsilon(1.0e-6)); + + auto q1_minus_q2 = q1 - q2; + REQUIRE(q1_minus_q2.unit() == q1.unit()); + REQUIRE(q1_minus_q2.scale() == Approx(1.0 - k2).epsilon(1.0e-6)); + + auto q1_neg = -q1; + REQUIRE(q1_neg.unit() == q1.unit()); + REQUIRE(q1_neg.scale() == -q1.scale()); + + auto q1_mult_k2 = q1 * k2; + auto k2_mult_q1 = k2 * q1; + REQUIRE(q1_mult_k2.unit() == q1.unit()); + REQUIRE(k2_mult_q1.unit() == q1.unit()); + REQUIRE(q1_mult_k2 == k2_mult_q1); + + /* expect q1*k2 ~ q2, but may not be exact */ + { + auto s = q1_mult_k2 / q2; + + REQUIRE(s.is_dimensionless()); + REQUIRE(s.scale() == Approx(1.0).epsilon(1.0e-6)); + } + } + } + } + + /* Strategy: + * 1. start with a set of basis units in each dimension. + * 2. verify +,- by combining quantities with different units + */ + TEST_CASE("xquantity.full", "[xquantity.full]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity.full")); + + // can get bits instead from /dev/random by uncommenting the line below in place of 2nd line + //rng::Seed seed; + uint64_t seed = 7032458451101515502; + + log && log(tag("seed", seed)); + + auto rng = xoshiro256ss(seed); + + quantity_tests(c_debug_flag, rng); + } /*TEST_CASE(xquantity.full)*/ + + TEST_CASE("xquantity", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity")); + + /* not constexpr until c++26 */ + auto ng = unit_qty(u::nanogram); + + log && log(xtag("ng", ng)); + + REQUIRE(ng.scale() == 1); + } /*TEST_CASE(xquantity)*/ + + TEST_CASE("xquantity2", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity2")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + auto ng2 = ng * ng; + + log && log(xtag("ng*ng", ng2)); + + REQUIRE(ng2.scale() == 1); + } /*TEST_CASE(xquantity2)*/ + + TEST_CASE("xquantity3", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity3")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + auto ng0 = ng / ng; + + log && log(xtag("ng/ng", ng0)); + + REQUIRE(ng0.scale() == 1); + } /*TEST_CASE(xquantity3)*/ + + TEST_CASE("xquantity4", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity4")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto prod1 = ng * ug; + log && log(xtag("ng*ug", prod1)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(prod1.unit().n_bpu() == 1); + REQUIRE(prod1.unit()[0].native_dim() == dim::mass); + REQUIRE(prod1.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000000)); + REQUIRE(prod1.unit()[0].power() == power_ratio_type(2, 1)); + REQUIRE(prod1.scale() == 1000); + } + + { + auto prod2 = ug * ng; + log && log(xtag("ug*ng", prod2)); + + REQUIRE(prod2.unit().n_bpu() == 1); + REQUIRE(prod2.unit()[0].native_dim() == dim::mass); + REQUIRE(prod2.unit()[0].native_dim() == dim::mass); + REQUIRE(prod2.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000)); + REQUIRE(prod2.unit()[0].power() == power_ratio_type(2, 1)); + REQUIRE(prod2.scale() == 0.001); + } + + //REQUIRE(ng2.scale() == 1); + } /*TEST_CASE(xquantity4)*/ + + TEST_CASE("xquantity5", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity5")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto ratio1 = ng / ug; + log && log(xtag("ng/ug", ratio1)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(ratio1.unit().n_bpu() == 0); + REQUIRE(ratio1.scale() == 0.001); + } + + { + auto ratio2 = ug / ng; + log && log(xtag("ug/ng", ratio2)); + + REQUIRE(ratio2.unit().n_bpu() == 0); + REQUIRE(ratio2.scale() == 1000.0); + } + + //REQUIRE(ng2.scale() == 1); + } /*TEST_CASE(xquantity5)*/ + + TEST_CASE("xquantity6", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity6")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto sum1 = ng + ug; + log && log(xtag("ng+ug", sum1)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(sum1.unit().n_bpu() == 1); + REQUIRE(sum1.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000000)); + REQUIRE(sum1.scale() == 1001.0); + } + + { + auto sum2 = ug + ng; + log && log(xtag("ug+ng", sum2)); + + /* units will be micrograms, since that's on rhs */ + REQUIRE(sum2.unit().n_bpu() == 1); + REQUIRE(sum2.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000)); + REQUIRE(sum2.scale() == 1.001); + } + + //REQUIRE(ng2.scale() == 1); + } /*TEST_CASE(xquantity6)*/ + + TEST_CASE("xquantity7", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity7")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto sum1 = ng - ug; + log && log(xtag("ng-ug", sum1)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(sum1.unit().n_bpu() == 1); + REQUIRE(sum1.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000000)); + REQUIRE(sum1.scale() == -999.0); + } + + { + auto sum2 = ug - ng; + log && log(xtag("ug-ng", sum2)); + + /* units will be micrograms, since that's on rhs */ + REQUIRE(sum2.unit().n_bpu() == 1); + REQUIRE(sum2.unit()[0].scalefactor() == scalefactor_ratio_type(1, 1000000)); + REQUIRE(sum2.scale() == 0.999); + } + + //REQUIRE(ng2.scale() == 1); + } /*TEST_CASE(xquantity7)*/ + + TEST_CASE("xquantity.compare", "[xquantity.compare]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity.compare")); + + /* not constexpr until c++26 */ + xquantity ng = 1000 * unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto cmp = (ng == ug); + log && log(xtag("ng==ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == true); + } + + { + auto cmp = (ng != ug); + log && log(xtag("ng!=ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == false); + } + + { + auto cmp = (ng < ug); + log && log(xtag("ng ug); + log && log(xtag("ng>ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == false); + } + + { + auto cmp = (ng >= ug); + log && log(xtag("ng>=ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == true); + } + + } /*TEST_CASE(xquantity.compare)*/ + + TEST_CASE("xquantity.compare2", "[xquantity]") { + constexpr bool c_debug_flag = false; + + scope log(XO_DEBUG2(c_debug_flag, "TEST_CASE.xquantity.compare2")); + + /* not constexpr until c++26 */ + xquantity ng = unit_qty(u::nanogram); + xquantity ug = unit_qty(u::microgram); + + { + auto cmp = (ng == ug); + log && log(xtag("ng==ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == false); + } + + { + auto cmp = (ng != ug); + log && log(xtag("ng!=ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == true); + } + + { + auto cmp = (ng < ug); + log && log(xtag("ng ug); + log && log(xtag("ng>ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == false); + } + + { + auto cmp = (ng >= ug); + log && log(xtag("ng>=ug", cmp)); + + /* units will be nanograms, since that's on lhs */ + REQUIRE(cmp == false); + } + + } /*TEST_CASE(xquantity.compare2)*/ + } /*namespace ut*/ +} /*namespace xo*/ + + +/* end xquantity.test.cpp */