Merge branch 'master' into chiphogg/mod#509

This commit is contained in:
Chip Hogg
2024-11-13 08:41:52 -05:00
39 changed files with 3381 additions and 2069 deletions

View File

@@ -7,3 +7,6 @@ ignore =
E712, E712,
# line break before binary operator # line break before binary operator
W503 W503
per-file-ignores =
# flake8 is just plain wrong here, contradicting black
.github/generate-job-matrix.py:E225,E231

206
.github/generate-job-matrix.py vendored Normal file
View File

@@ -0,0 +1,206 @@
import argparse
import json
import os
import random
import typing
from types import SimpleNamespace
from job_matrix import CombinationCollector, Compiler, Configuration
def make_gcc_config(version: int) -> Configuration:
return Configuration(
name=f"GCC-{version}",
os="ubuntu-24.04",
compiler=Compiler(
type="GCC",
version=version,
cc=f"gcc-{version}",
cxx=f"g++-{version}",
),
cxx_modules=False,
std_format_support=version >= 13,
)
def make_clang_config(
version: int, platform: typing.Literal["x86-64", "arm64"] = "x86-64"
) -> Configuration:
cfg = SimpleNamespace(
name=f"Clang-{version} ({platform})",
compiler=SimpleNamespace(
type="CLANG",
version=version,
),
lib="libc++",
cxx_modules=version >= 17,
std_format_support=version >= 17,
)
match platform:
case "x86-64":
cfg.os = "ubuntu-22.04" if version < 17 else "ubuntu-24.04"
cfg.compiler.cc = f"clang-{version}"
cfg.compiler.cxx = f"clang++-{version}"
case "arm64":
cfg.os = "macos-14"
pfx = f"/opt/homebrew/opt/llvm@{version}/bin"
cfg.compiler.cc = f"{pfx}/clang"
cfg.compiler.cxx = f"{pfx}/clang++"
case _:
raise KeyError(f"Unsupported platform {platform!r} for Clang")
ret = cfg
ret.compiler = Compiler(**vars(cfg.compiler))
return Configuration(**vars(ret))
def make_apple_clang_config(version: int) -> Configuration:
ret = Configuration(
name=f"Apple Clang {version}",
os="macos-13",
compiler=Compiler(
type="APPLE_CLANG",
version=f"{version}.0",
cc="clang",
cxx="clang++",
),
cxx_modules=False,
std_format_support=False,
)
return ret
def make_msvc_config(release: str, version: int) -> Configuration:
ret = Configuration(
name=f"MSVC {release}",
os="windows-2022",
compiler=Compiler(
type="MSVC",
version=version,
cc="",
cxx="",
),
cxx_modules=False,
std_format_support=True,
)
return ret
configs = {
c.name: c
for c in [make_gcc_config(ver) for ver in [12, 13, 14]]
+ [
make_clang_config(ver, platform)
for ver in [16, 17, 18]
for platform in ["x86-64", "arm64"]
# arm64 runners are expensive; only consider one version
if ver == 18 or platform != "arm64"
]
+ [make_apple_clang_config(ver) for ver in [15]]
+ [make_msvc_config(release="14.4", version=194)]
}
full_matrix = dict(
config=list(configs.values()),
std=[20, 23],
formatting=["std::format", "fmtlib"],
contracts=["none", "gsl-lite", "ms-gsl"],
build_type=["Release", "Debug"],
)
def main():
parser = argparse.ArgumentParser()
# parser.add_argument("-I","--include",nargs="+",action="append")
# parser.add_argument("-X","--exclude",nargs="+",action="append")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--preset", default=None)
parser.add_argument("--debug", nargs="+", default=["combinations"])
parser.add_argument("--suppress-output", default=False, action="store_true")
args = parser.parse_args()
rgen = random.Random(args.seed)
collector = CombinationCollector(
full_matrix,
hard_excludes=lambda e: (
e.formatting == "std::format" and not e.config.std_format_support
),
)
match args.preset:
case None:
pass
case "all":
collector.all_combinations()
case "conan" | "cmake":
collector.all_combinations(
formatting="std::format",
contracts="gsl-lite",
build_type="Debug",
std=20,
)
collector.all_combinations(
filter=lambda me: not me.config.std_format_support,
formatting="fmtlib",
contracts="gsl-lite",
build_type="Debug",
std=20,
)
collector.sample_combinations(rgen=rgen, min_samples_per_value=2)
case "clang-tidy":
collector.all_combinations(config=configs["Clang-18 (x86-64)"])
case "freestanding":
# TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler
collector.all_combinations(
filter=lambda e: not (
e.config.name.startswith("Clang-18") and e.build_type == "Debug"
),
config=[configs[c] for c in ["GCC-14", "Clang-18 (x86-64)"]],
contracts="none",
std=23,
)
case _:
raise KeyError(f"Unsupported preset {args.preset!r}")
if not collector.combinations:
raise ValueError("No combination has been produced")
data = sorted(collector.combinations)
json_data = [e.as_json() for e in data]
output_file = os.environ.get("GITHUB_OUTPUT")
if not args.suppress_output:
if output_file:
print(f"Writing outputs to {output_file}")
with open(output_file, "wt") as fh:
fh.write(f"matrix={json.dumps(json_data)}")
else:
print("No output file received!")
for dbg in args.debug:
match dbg:
case "yaml":
import yaml
json_data = json.loads(json.dumps(json_data))
print(yaml.safe_dump(json_data))
case "json":
print(json.dumps(json_data, indent=4))
case "combinations":
for e in data:
print(
f"{e.config!s:17s} c++{e.std:2d} {e.formatting:11s} {e.contracts:8s} {e.build_type:8s}"
)
case "counts":
print(f"Total combinations {len(data)}")
for (k, v), n in sorted(collector.per_value_counts.items()):
print(f" {k}={v}: {n}")
case "none":
pass
case _:
raise KeyError(f"Unknown debug mode {dbg!r}")
if __name__ == "__main__":
main()

139
.github/job_matrix.py vendored Normal file
View File

@@ -0,0 +1,139 @@
import dataclasses
import itertools
import random
import typing
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Compiler:
type: typing.Literal["GCC", "CLANG", "APPLE_CLANG", "MSVC"]
version: str | int
cc: str
cxx: str
@dataclass(frozen=True, order=True)
class Configuration:
name: str
os: str
compiler: Compiler
cxx_modules: bool
std_format_support: bool
conan_config: str = ""
lib: typing.Literal["libc++", "libstdc++"] | None = None
def __str__(self):
return self.name
@dataclass(frozen=True, order=True)
class MatrixElement:
config: Configuration
std: typing.Literal[20, 23]
formatting: typing.Literal["std::format", "fmtlib"]
contracts: typing.Literal["none", "gsl-lite", "ms-gsl"]
build_type: typing.Literal["Release", "Debug"]
def as_json(self):
def dataclass_to_json(obj):
"""Convert dataclasses to something json-serialisable"""
if dataclasses.is_dataclass(obj):
return {
k: dataclass_to_json(v) for k, v in dataclasses.asdict(obj).items()
}
return obj
ret = dataclass_to_json(self)
# patch boolean conan configuration options
config = ret["config"]
for k in ["cxx_modules"]:
config[k] = "True" if config[k] else "False"
return ret
class CombinationCollector:
"""Incremental builder of MatrixElements, allowing successive selection of entries."""
def __init__(
self,
full_matrix: dict[str, list[typing.Any]],
*,
hard_excludes: typing.Callable[[MatrixElement], bool] | None = None,
):
self.full_matrix = full_matrix
self.hard_excludes = hard_excludes
self.combinations: set[MatrixElement] = set()
self.per_value_counts: dict[tuple[str, typing.Any], int] = {
(k, v): 0 for k, options in full_matrix.items() for v in options
}
def _make_submatrix(self, **overrides):
new_matrix = dict(self.full_matrix)
for k, v in overrides.items():
if not isinstance(v, list):
v = [v]
new_matrix[k] = v
return new_matrix
def _add_combination(self, e: MatrixElement):
if e in self.combinations or (
self.hard_excludes is not None and self.hard_excludes(e)
):
return
self.combinations.add(e)
# update per_value_counts
for k, v in vars(e).items():
idx = (k, v)
self.per_value_counts[idx] = self.per_value_counts.get(idx, 0) + 1
def all_combinations(
self,
*,
filter: typing.Callable[[MatrixElement], bool] | None = None,
**overrides,
):
"""Adds all combinations in the submatrix defined by `overrides`."""
matrix = self._make_submatrix(**overrides)
keys = tuple(matrix.keys())
for combination in itertools.product(*matrix.values()):
cand = MatrixElement(**dict(zip(keys, combination)))
if filter and not filter(cand):
continue
self._add_combination(cand)
def sample_combinations(
self,
*,
rgen: random.Random,
min_samples_per_value: int = 1,
filter: typing.Callable[[MatrixElement], bool] | None = None,
**overrides,
):
"""Adds samples from the submatrix defined by `overrides`,
ensuring each individual value appears at least n times.
"""
matrix = self._make_submatrix(**overrides)
missing: dict[tuple[str, typing.Any], int] = {}
for key, options in matrix.items():
for value in options:
idx = (key, value)
missing[idx] = min_samples_per_value - self.per_value_counts.get(idx, 0)
while missing:
(force_key, force_option), remaining = next(iter(missing.items()))
if remaining <= 0:
missing.pop((force_key, force_option))
continue
choice = {}
for key, options in matrix.items():
choice[key] = force_option if key == force_key else rgen.choice(options)
cand = MatrixElement(**choice)
if filter and not filter(cand):
continue
self._add_combination(cand)
for idx in choice.items():
if missing.pop(idx, 0) <= 0:
continue
remaining = min_samples_per_value - self.per_value_counts.get(idx, 0)
if remaining > 0:
missing[idx] = remaining

View File

@@ -34,33 +34,33 @@ on:
paths-ignore: paths-ignore:
- "docs/**" - "docs/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset clang-tidy --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
build_type: ["Release", "Debug"]
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}
@@ -127,8 +127,11 @@ jobs:
sed -i.backup '/^\[settings\]$/,/^\[/ s/^compiler.cppstd=.*/compiler.cppstd=${{ matrix.std }}/' ~/.conan2/profiles/default sed -i.backup '/^\[settings\]$/,/^\[/ s/^compiler.cppstd=.*/compiler.cppstd=${{ matrix.std }}/' ~/.conan2/profiles/default
sed -i.backup '/^\[settings\]$/,/^\[/ s/^build_type=.*/build_type=${{ matrix.build_type }}/' ~/.conan2/profiles/default sed -i.backup '/^\[settings\]$/,/^\[/ s/^build_type=.*/build_type=${{ matrix.build_type }}/' ~/.conan2/profiles/default
conan profile show -pr default conan profile show -pr default
- run: echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV - name: Set 'std_format' and 'import_std' environment variables
- run: echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV shell: bash
run: |
echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Run clang-tidy - name: Run clang-tidy
shell: bash shell: bash
run: | run: |

View File

@@ -33,149 +33,35 @@ on:
env: env:
CHANNEL: ${{ fromJSON('["testing", "stable"]')[github.ref_type == 'tag' && startsWith(github.ref_name, 'v')] }} CHANNEL: ${{ fromJSON('["testing", "stable"]')[github.ref_type == 'tag' && startsWith(github.ref_name, 'v')] }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "MSVC 14.4",
os: windows-2022,
compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "GCC-12",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 12,
cc: "gcc-12",
cxx: "g++-12",
},
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
- {
name: "GCC-13",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 13,
cc: "gcc-13",
cxx: "g++-13",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-16",
os: ubuntu-22.04,
compiler:
{
type: CLANG,
version: 16,
cc: "clang-16",
cxx: "clang++-16",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
- {
name: "Clang-17",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 17,
cc: "clang-17",
cxx: "clang++-17",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18 on Apple M1 (arm64)",
os: macos-14,
compiler:
{
type: CLANG,
version: 18,
cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Apple Clang 15",
os: macos-13,
compiler:
{
type: APPLE_CLANG,
version: "15.0",
cc: "clang",
cxx: "clang++",
},
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
build_type: ["Release", "Debug"]
exclude:
- formatting: "std::format"
config: { std_format_support: "False" }
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}
CXX: ${{ matrix.config.compiler.cxx }} CXX: ${{ matrix.config.compiler.cxx }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Generate unique cache id - name: Generate unique cache id
@@ -258,21 +144,21 @@ jobs:
shell: bash shell: bash
run: | run: |
echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Create Conan package - name: Create Conan package
if: matrix.config.compiler.type != 'MSVC' if: matrix.config.compiler.type != 'MSVC'
shell: bash shell: bash
run: | run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \ conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=True \ -b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=True \
-o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }} -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Create Conan package - name: Create Conan package
if: matrix.config.compiler.type == 'MSVC' if: matrix.config.compiler.type == 'MSVC'
shell: bash shell: bash
run: | run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \ conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=False \ -b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=False \
-o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }} -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Obtain package reference - name: Obtain package reference
id: get-package-ref id: get-package-ref
shell: bash shell: bash

View File

@@ -24,6 +24,10 @@ name: Formatting CI
on: [push, pull_request] on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check: check:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View File

@@ -34,51 +34,32 @@ on:
paths-ignore: paths-ignore:
- "docs/**" - "docs/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset freestanding --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none"]
std: [23]
config:
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
build_type: ["Release", "Debug"]
# TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler
exclude:
- build_type: "Debug"
config: { name: "Clang-18" }
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}

View File

@@ -38,136 +38,32 @@ on:
- "example/**" - "example/**"
- "test/**" - "test/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
test_package: test_package:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "MSVC 14.4",
os: windows-2022,
compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "GCC-12",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 12,
cc: "gcc-12",
cxx: "g++-12",
},
cxx_modules: "False",
std_format_support: "False",
}
- {
name: "GCC-13",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 13,
cc: "gcc-13",
cxx: "g++-13",
},
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Clang-16",
os: ubuntu-22.04,
compiler:
{
type: CLANG,
version: 16,
cc: "clang-16",
cxx: "clang++-16",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "False",
}
- {
name: "Clang-17",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 17,
cc: "clang-17",
cxx: "clang++-17",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Clang-18 on Apple M1 (arm64)",
os: macos-14,
compiler:
{
type: CLANG,
version: 18,
cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Apple Clang 15",
os: macos-14,
compiler:
{
type: APPLE_CLANG,
version: "15.0",
cc: "clang",
cxx: "clang++",
},
cxx_modules: "False",
std_format_support: "False",
}
build_type: ["Release", "Debug"]
exclude:
- formatting: "std::format"
config: { std_format_support: "False" }
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}
@@ -255,8 +151,8 @@ jobs:
- name: Set 'std_format' and 'import_std' environment variables - name: Set 'std_format' and 'import_std' environment variables
shell: bash shell: bash
run: | run: |
echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV echo "std_format=$([ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
echo "import_std=$([ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV echo "import_std=$([ "${{ matrix.std }}" -ge "23" ] && [ "${{ matrix.config.cxx_modules }}" == "True" ] && [ "${{ matrix.config.contracts }}" == "none" ] && [ "${{ matrix.formatting }}" == "std::format" ] && echo "True" || echo "False")" >> $GITHUB_ENV
- name: Install Conan dependencies - name: Install Conan dependencies
shell: bash shell: bash
run: | run: |

View File

@@ -5,6 +5,10 @@ on:
paths: paths:
- CITATION.cff - CITATION.cff
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
Validate-CITATION-cff: Validate-CITATION-cff:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -22,6 +22,10 @@ on:
paths-ignore: paths-ignore:
- "docs/**" - "docs/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze

View File

@@ -36,6 +36,11 @@ on:
- "mkdocs.yml" - "mkdocs.yml"
permissions: permissions:
contents: write contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -26,6 +26,7 @@ In this series, we will describe:
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md) - [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md) - [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md) - [Part 5 - Benefits](isq-part-5-benefits.md)
- [Part 6 - Challenges](isq-part-6-challenges.md)
## Terms and Definitions ## Terms and Definitions

View File

@@ -28,6 +28,7 @@ library on just units or dimensions.
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md) - [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md) - [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md) - [Part 5 - Benefits](isq-part-5-benefits.md)
- [Part 6 - Challenges](isq-part-6-challenges.md)
## Limitations of units-only solutions ## Limitations of units-only solutions

View File

@@ -27,6 +27,7 @@ language.
- Part 3 - Modeling ISQ - Part 3 - Modeling ISQ
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md) - [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md) - [Part 5 - Benefits](isq-part-5-benefits.md)
- [Part 6 - Challenges](isq-part-6-challenges.md)
## Dimension is not enough to describe a quantity ## Dimension is not enough to describe a quantity

View File

@@ -28,6 +28,7 @@ Now, it is time to see how we can implement hierarchies of quantities of the sam
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md) - [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- Part 4 - Implementing ISQ - Part 4 - Implementing ISQ
- [Part 5 - Benefits](isq-part-5-benefits.md) - [Part 5 - Benefits](isq-part-5-benefits.md)
- [Part 6 - Challenges](isq-part-6-challenges.md)
## Modeling a hierarchy of kind _length_ ## Modeling a hierarchy of kind _length_

View File

@@ -26,6 +26,7 @@ how our ISQ model elegantly addresses the remaining problems.
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md) - [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md) - [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- Part 5 - Benefits - Part 5 - Benefits
- [Part 6 - Challenges](isq-part-6-challenges.md)
## Generic but safe interfaces ## Generic but safe interfaces

View File

@@ -0,0 +1,530 @@
---
date: 2024-11-11
authors:
- mpusz
categories:
- Metrology
comments: true
---
# International System of Quantities (ISQ): Part 6 - Challenges
This article might be the last one from our series. This time, we will discuss the challenges and
issues with modeling of the ISQ in software.
<!-- more -->
## Articles from this series
- [Part 1 - Introduction](isq-part-1-introduction.md)
- [Part 2 - Problems when ISQ is not used](isq-part-2-problems-when-isq-is-not-used.md)
- [Part 3 - Modeling ISQ](isq-part-3-modeling-isq.md)
- [Part 4 - Implementing ISQ](isq-part-4-implemeting-isq.md)
- [Part 5 - Benefits](isq-part-5-benefits.md)
- Part 6 - Challenges
## Ambiguity
Some quantity names are ambiguous. It is not a problem of ISQ but of the English language and
the way we communicate things. When I say: "Every _width_ is a _length_, but not every _length_
is a _width_" most people understand this right away. However, the same people trying to model
[our 3D box problem](isq-part-2-problems-when-isq-is-not-used.md#various-quantities-of-the-same-dimension-and-kinds)
try to do it as follows:
```cpp
class Box {
quantity<isq::length[m]> length_;
quantity<isq::width[m]> width_;
quantity<isq::height[m]> height_;
public:
// ...
};
```
This looks correct at first sight. Only when we think about the sentence mentioned above will
we realize that this implementation has a problem. We intended to specify three orthogonal
dimensions of the box, each of which will be a strong quantity that is not convertible to others.
But we've failed.
When we look at the
[tree of quantities of length](isq-part-4-implemeting-isq.md#modeling-a-hierarchy-of-kind-length)
we immediately see that both _width_ and _height_ are special _lengths_ so they are convertible to
it.
To implement our task correctly, we had to define and use a new quantity of kind _length_:
```cpp
inline constexpr struct horizontal_length final : quantity_spec<isq::length> {} horizontal_length;
```
We do not propose adding _horizontal length_ to ISO 80000-3. There are probably other similar
cases as well, but so far, this was the most common and obvious one we've encountered.
## No common quantities
ISO 80000-1:2009 explicitly states:
!!! quote
Two or more quantities cannot be added or subtracted unless they belong to the same category
of mutually comparable quantities.
This means that we should be able to add and subtract any quantities as long as they belong to
the same kind. However, ISO/IEC documents do not provide any rules or even hints about what should
be the result of such operations.
If it is possible to add _radius_ and _distance_, then what quantity should be provided
in return? Undoubtedly, the resulting quantity type can't be the same as any of the arguments.
It is not a _radius_ or _distance_. It is some closer unspecified _length_, though.
!!! info
Finding the correct solution took us many months of experimentation and implementation.
Based on the hierarchy tree of quantities, we can define
[conversion rules](isq-part-3-modeling-isq.md#converting-between-quantities-of-the-same-kind)
and what a [common quantity should be](isq-part-3-modeling-isq.md#comparing-adding-and-subtracting-quantities-of-the-same-kind).
## Lack of consistency
The documents of ISO/IEC 80000 are not 100% consistent, and programming languages do not like
inconsistencies.
For example:
- _time_ is mentioned as a base quantity of ISQ in ISO 80000-1 chapter 4.5.
- ISO 80000-3 "Space and time", does not define a quantity of _time_. It provides a _duration_
quantity (item 3-9) with symbol _t_, and states in the Remarks section:
!!! quote
Duration is often just called time.
- Other parts (e.g., IEC 80000-6 "Electromagnetism") often say:
!!! quote
... _t_ is time (ISO 80000-3)
To be consistent, ISO/IEC should either:
- change ISO 80000-1 chapter 4.5 and all references in other parts to use _duration_ (unlikely),
- or add _time_ as an alias name to _duration_ in the definition 3-9 of ISO 80000-3.
## Lack of definitions
ISQ defines derived quantities in terms of other quantities provided in the series. However, some
definitions mention quantities that are not defined in the ISQ at all.
For example, _weight_ is defined as $F_\textsf{g} = m\;g$, where $m$ is the _mass_ of the body
(item 4-1 of ISO 80000-4 "Mechanics"), and $g$ is the _local acceleration of free fall_ (ISO 80000-3).
The problem here is that ISO 80000-3 never defines a quantity with a symbol $g$ or named as a
_local acceleration of free fall_. The closest one we have is _acceleration_ (item 3-11) with
a symbol $a$.
!!! info
To have a proper definition of _weight_ in **mp-units** that is not defined in terms of just
any kind of _acceleration_, we have added `isq::acceleration_of_free_fall` in our definitions
as an extension to the original ISQ set of quantities.
## Not engineering-friendly
Many quantities have proper physical definitions, but they are sometimes not engineering-friendly.
For example, _velocity_ is defined as a rate of change of _position vector_
$v = \frac{\textsf{d}r}{\textsf{d}t}$, where $r$ denotes the _position vector_ (item 31.10) and
$t$ the _duration_ (item 39).
Next, a _speed_ quantity is defined as the magnitude of _velocity_. Despite being
physically correct, requiring every _speed_ to be derived from the vector quantity of _velocity_
in software would be inconvenient. If this was the only case, people would always need to use
vector representations of _position vectors_ to talk about _speeds_, which differs from what we
do in practice. In practice, we divide any kind of _length_ by _time_ to get some kind of _speed_.
ISO 80000-3 provides _length_, _height_, _distance_ and other quantities of kind _length_ that when
divided by _duration_ can serve really well to calculate _speed_.
!!! info
This is why in **mp-units**, we decided to divert from the official definition of _speed_ and
define it as:
```cpp
inline constexpr struct speed : quantity_spec<speed, length / time> {} speed;
```
This allows us to create a quantity of kind _speed_ from any quantity of _length_ divided
by _time_.
Additionally, it is essential to note that for the needs of our library, defining _velocity_
as `position_vector / duration` would be wrong. We miss the delta part here. Even though it is
not mentioned in ISO 80000-3, the delta of _position vectors_ is actually a _displacement_.
This is why our _velocity_ is defined as:
```cpp
inline constexpr struct velocity : quantity_spec<speed, displacement / duration> {} velocity;
```
Please also note that _velocity_ is defined as a more specialized quantity of _speed_.
## Affine space agnostic
[The affine space](../../users_guide/framework_basics/the_affine_space.md) is a powerful
abstraction, allowing us to model some problems safer or more accurately. It has two types of
entities:
- point - a position specified with coordinate values (e.g., location, address, etc.),
- displacement vector - the difference between two points (e.g., shift, offset, displacement,
duration, etc.).
Vectors support all the arithmetics operations, but points have some limitations. It is not
possible to:
- add two _points_,
- subtract a _point_ from a _vector_,
- multiply nor divide _points_ with anything else.
ISO/IEC series does not acknowledge this abstraction even though it would be really useful in
some cases. Let's discuss the following two examples.
What does it mean to add two _altitudes_? It is not meaningful. On the other hand, subtracting
those should not result in an _altitude_, but in a quantity of _height_. Adding or
subtracting _height_ to/from _altitude_ results in _altitude_. Subtracting _altitude_ from
_height_ is meaningless again. Those quantities clearly model affine space. Maybe this is why
ISQ defines them as one quantity type _height_/_depth_/_altitude_?
What does it mean to add two _position vectors_? It is not meaningful again. However, subtracting
those results in a _displacement_ as we noted in the previous chapter. Adding or subtracting
_displacement_ to/from _position vector_ results in another _position vector_, and subtracting
_position vector_ from _displacement_ does not have physical sense. Again, those quantities
perfectly model affine space. However, this time, those are defined as separate and independent
quantities (i.e., displacement is not modeled as delta _position vector_ or _position vector_
is not modeled as a _displacement_ from the origin of a coordinate system).
!!! info
Currently, **mp-units** does not enforce the affine space behavior for such quantities.
Today, subtracting two _altitudes_ result in an _altitude_ and subtracting two
_position vectors_ result in a _position vector_. However, we plan to support automatic
conversion to a proper quantity type on subtraction and addition shortly.
## Non-negative quantities
Some quantities in the ISQ are defined as non-negative. This is a really interesting property that
may be checked at runtime to increase safety. However, the number of such quantities is minimal.
From a few hundred quantities provided by the ISO/IEC series, only the following have this property
mentioned explicitly:
- _width_/_breadth_,
- _thickness_,
- _diameter_,
- _radius_.
If _height_ was defined separately from _altitude_, it could probably also join this group.
Let's think a bit more about this. What does it mean that a quantity is non-negative? Indeed,
it is hard to imagine something of a negative _width_ or _radius_. However, if we subtract
two _widths_, the second one may be larger. This will result in a negative
quantity of _width_, violating our precondition. So, is it non-negative or not?
Again, we have to talk about the affine space abstractions. Every empirical measurement can be
expressed as a point. Such points for some quantities may be non-negative indeed.
Non-negative quantities do not end on the ones provided above. For example, _speed_ is a good
example here as well. In general, all magnitudes of vector quantities will also have this property.
When subtracting two points, we end up with a delta/displacement type, which may be negative
even for quantities listed as non-negative in the ISQ. As stated in the previous chapter,
having affine space abstractions acknowledged in ISQ would greatly help here.
## Lack of quantity recipes
Definition of many derived quantities provides their recipes in the form of
[quantity equations](../../appendix/glossary.md#quantity-equation) (e.g., _weight_ equation
in the previous chapter). However, some of them do not. Instead, they often provide a very
generic description.
For example, _force_ is defined as:
!!! quote
vector (ISO 80000-2) quantity describing interaction between bodies or particles.
This is not helpful for programming languages that like explicit definitions. Different
vendors may interpret the above differently, which will result in different implementations that
will not be compatible with each other.
As the derived quantity of _force_ has to be a vector quantity, it has to be defined in terms of
at least one other vector quantity. We have a few to choose from:
- _displacement_ ($\Delta{r}$),
- _velocity_ ($v$),
- _acceleration_ ($a$).
It is not stated explicitly in ISQ which one of those should be used and how.
!!! info
In **mp-units** we decided to define _force_ as $F = m\;a$.
## Lack of generic quantities and name conflicts
In the previous chapter, we complained about some definitions needing to be more complex or generic.
On the other hand, we also lack some generic quantities in ISQ that could serve as a root for
a quantity hierarchy tree.
For example:
- ISO 80000-4 "Mechanics" defines _power &lt;mechanics&gt;_ as $P = F\;v$ (scalar product of force $F$
(item 4-9.1) acting to a body and its velocity $v$ (ISO 80000-3)),
- ISO 80000-6 "Electromagnetism" defines _power_ as $p = u\;i$ (scalar quantity given by the
product of _instantaneous voltage_ $u$ (item 6-11.3) and _instantaneous electric current_ $i$
(item 6-1)).
First, the above definitions have somehow conflicting names which makes it hard for the programming
languages to name them consistently by different vendors.
!!! info
In **mp-units**, we chose `mechanical_power` and `electromagnetism_power` for those.
Second, we do not have any other more generic definition of _power_ to put above those in the tree.
Not having it makes it hard to answer what should be the result of:
```cpp
quantity q = isq::mechanical_power(42 * W) + isq::electromagnetism_power(60 * W);
```
!!! info
To solve the above problem, we have added `isq::power` in **mp-units**, that has a really
generic definition of:
```cpp
inline constexpr struct power : quantity_spec<mass* pow<2>(length) / pow<3>(time)> {} power;
```
## Invalid definitions order
_Energy_ is defined a bit better than _power_, but still not without issues.
The first time ISQ mentions _energy_ is in the ISO 80000-4 "Mechanics". It defines
_potential energy_, _kinetic energy_, and a _mechanical energy_ as the sum of the first two.
Right after that a _mechanical work/work_ is defined.
Then ISO 80000-5 "Thermodynamics" defines _energy &lt;thermodynamics&gt;_ as:
!!! quote
ability of a system to do work (ISO 80000-4).
Next, _internal energy/thermodynamic energy_ is defined in terms of the change of heat.
From the above, it seems that what is called _energy &lt;thermodynamics&gt;_ should actually be
the root of our tree and probably be provided in Part 4 before the _mechanical energy_ is defined.
## Hierarchies of derived quantities
Derived quantities of the same kind are often independently defined in the ISQ. The ISO/IEC 80000
series often does not suggest any hierarchy between those. Even more, it states:
!!! quote "ISO/IEC Guide 99"
The division of quantity according to kind of quantity is, to some extent, arbitrary.
Because of this, it is unknown or ambiguous how to form a hierarchy tree for such quantities.
To get some sense of the complexity here, let's look again at our tree of quantities of a kind
_energy_:
```mermaid
flowchart TD
energy["<b>energy</b><br><i>(mass * length<sup>2</sup> / time<sup>2</sup>)</i><br>[J]"]
energy --- mechanical_energy["<b>mechanical_energy</b>"]
mechanical_energy --- potential_energy["<b>potential_energy</b>"]
mechanical_energy --- kinetic_energy["<b>kinetic_energy</b>"]
energy --- enthalpy["<b>enthalpy</b>"]
enthalpy --- internal_energy["<b>internal_energy</b> / <b>thermodynamic_energy</b>"]
internal_energy --- Helmholtz_energy["<b>Helmholtz_energy</b> / <b>Helmholtz_function</b>"]
enthalpy --- Gibbs_energy["<b>Gibbs_energy</b> / <b>Gibbs_function</b>"]
energy --- active_energy["<b>active_energy</b>"]
```
Not being exact means that every vendor may implement it differently. This will result in:
- different convertibility rules among quantities:
```cpp
static_assert(implicitly_convertible(isq::potential_energy, isq::mechanical_energy));
static_assert(explicitly_convertible(isq::mechanical_energy, isq::potential_energy));
```
- different common quantities resulting from the arithmetics on various quantities of the same
kind:
```cpp
static_assert((isq::potential_energy(1 * J) + isq::kinetic_energy(1 * J)).quantity_spec == isq::mechanical_energy);
```
It would be great if ISQ could provide specific division of quantities into kinds and more
information about the position of each quantity within the hierarchy of quantities of
the same kind.
!!! important
We can try to do this by ourselves, but it is tough. Probably no one, for sure we are
not, is an expert in all the fields of ISO/IEC 80000 applicability.
We need the help of subject matter experts who will help us build those trees for their domains
and then verify that everything works as expected.
## The same or a different kind?
Some quantities are more complicated than others. For example, _power_ has:
- scalar quantities expressed in:
- W (watts) (e.g., _mechanical power_, _active power_),
- VA (volt-ampere) (e.g., _apparent power_),
- var (e.g., _reactive power_),
- complex quantities expressed in VA (volt-ampere) (e.g., _complex power_).
How should we model this? Maybe those should be two or three independent trees of quantities, each
having its own unit?
```mermaid
flowchart TD
power["<b>power</b><br><i>(mass * length<sup>2</sup> / time<sup>3</sup>)</i><br>[W]"]
power --- mechanical_power["<b>mechanical_power</b><br><i>(scalar_product(force, velocity))</i>"]
power --- electromagnetism_power["<b>electromagnetism_power</b> | <b>instantaneous_power</b><br><i>(instantaneous_voltage * instantaneous_electric_current)</i>"]
power --- active_power["<b>active_power</b><br><i>(1 / period * instantaneous_power * time)<br>(re(complex_power))</i>"]
nonactive_power["<b>nonactive_power</b><br><i>(mass * length<sup>2</sup> / time<sup>3</sup>)</i><br>[VA]"]
nonactive_power --- reactive_power["<b>reactive_power</b><br><i>(im(complex_power))</i><br>[var]"]
complex_power["<b>complex_power</b><br>{complex}<br><i>(voltage_phasor * electric_current_phasor)<br>(active_power + j * reactive_power)</i><br>[VA]"]
complex_power --- apparent_power["<b>apparent_power</b><br><i>(voltage * electric_current)<br>(mod(complex_power))</i>"]
```
This will mean that we will not be able to add or compare _active power_, _reactive power_, and
_apparent power_, which probably makes a lot of sense. However, it also means that the following
will fail to compile:
```cpp
quantity apparent = isq::apparent_power(100 * VA);
quantity active = isq::active_power(60 * W);
quantity<isq::nonactive_power[VA]> q = sqrt(pow<2>(apparent) - pow<2>(active)); // Compile-time error
```
Also the following will not work:
```cpp
quantity active = isq::active_power(60 * W);
quantity reactive = isq::reactive_power(40 * var);
quantity<isq::apparent_power[VA]> q = sqrt(pow<2>(active) + pow<2>(reactive)); // Compile-time error
```
If we want the above to work maybe we need to implement the tree as follows?
```mermaid
flowchart TD
power["<b>power</b><br><i>(mass * length<sup>2</sup> / time<sup>3</sup>)</i><br>[W]"]
power --- mechanical_power["<b>mechanical_power</b><br><i>(scalar_product(force, velocity))</i>"]
power --- electromagnetism_power["<b>electromagnetism_power</b> | <b>instantaneous_power</b><br><i>(instantaneous_voltage * instantaneous_electric_current)</i>"]
power --- apparent_power["<b>apparent_power</b><br><i>(voltage * electric_current)<br>(mod(complex_power))</i><br>[VA]"]
apparent_power --- active_power["<b>active_power</b><br><i>(1 / period * instantaneous_power * time)<br>(re(complex_power))</i>"]
apparent_power --- nonactive_power["<b>nonactive_power</b><br><i>(sqrt(apparent_power<sup>2</sup> - active_power<sup>2</sup>))</i><br>"]
nonactive_power --- reactive_power["<b>reactive_power</b><br><i>(im(complex_power))</i><br>[var]"]
apparent_power --- complex_power["<b>complex_power</b><br>{complex}<br><i>(voltage_phasor * electric_current_phasor)<br>(active_power + j * reactive_power)</i>"]
```
However, the above allows direct addition and comparison of _active power_ and _nonactive power_,
and also will not complain if someone will try to use watt (W) as a unit of _apparent power_ or
_reactive power_.
Again, ISQ does not provide a direct answer here.
## More base quantities?
Is ISQ really based on only seven base quantities? Let's look at the definition of
_traffic intensity_ in IEC 80000-13 "Information science and technology":
!!! quote
number of simultaneously busy resources in a particular pool of resources.
It looks like a definition of a specialized dimensionless quantity or, more correctly, a quantity
of dimension one. This would not be the only such case. Even in the same Part 13, we can find
quantities like _storage capacity_ with a similar property.
Only when we look closer do we start to see differences. All dimensionless quantities, even if they
have their own dedicated units, can also be measured in a unit of one (1). This is true for
_storage capacity_ (also measured in bits), _angular measure_ (also measured in radians),
_solid angular measure (also measured in steradians), and more.
However, _traffic intensity_ can only be measured in erlangs (E), not in a unit one (1).
Does it mean that it is a "hidden" 8-th base quantity in ISQ? If so, should it have its own
dimension as well?
Angular quantities are another interesting case here. Scientists have written petitions and papers
for years to make them an additional dimension in ISQ and SI. More about this can be found in
our documentation's [Strong Angular System](../../users_guide/systems/strong_angular_system.md)
chapter.
## Summary
ISQ is tremendous and solves many problems we had in modeling various subjects for years in
software. As a result, we have more powerful tools in our hands that allow us to deliver safer
products.
Unfortunately, ISQ, contrarily to SI, is not widely recognized, and no libraries besides
**mp-units** model it in any programming language. Keeping it behind a paywall does not help
either. We hope that posts from this series will spread in the community, raise awareness
of ISQ and its benefits, and encourage authors of other libraries to implement it in their
products.
Despite all the benefits, it is essential to realize that ISQ has many problems. International
standards should be specified in such a way that there is no room for ambiguity in their
interpretation by different parties trying to use them. As described above, this is not the case
here.
ISQ is not ready to be unambiguously modeled in software by various vendors. Here are the most
important problems to solve to allow this:
1. ISQ needs to define basic operations on quantities:
    - what the result of addition and subtraction should be when arguments differ,
    - convertibility rules.
2. The exact quantity equation recipe needs to be included for many derived quantities.
3. Many ISQ quantities do not provide their exact relation versus other quantities of the same
kind (no strict hierarchy).
4. Some missing quantities need to be included. Others would benefit from corrected names.
Additionally:
- extending ISQ with affine space abstractions,
- specifying more quantities as non-negative,
- adding more base quantities (i.e., _angle_)
could improve the safety of our programs and products that people depend on with their lives on
a daily basis.
I hope you enjoyed following this series and learned more about the International System
of Quantities. Please try it out in your domain and share feedback with us. We always love to
hear about the projects in which our library is being used and about use cases it helps
address.

View File

@@ -16,19 +16,11 @@ work in practice.
--8<-- "example/si_constants.cpp:28:40" --8<-- "example/si_constants.cpp:28:40"
``` ```
As always, we start with the inclusion of all the needed header files. After that, for As always, we start with the inclusion of all the needed header files.
the simplicity of this example, we
[hack the character of quantities](../framework_basics/character_of_a_quantity.md#hacking-the-character)
to be able to express vector quantities with simple scalar types.
```cpp title="si_constants.cpp" linenums="14"
--8<-- "example/si_constants.cpp:42:44"
```
The main part of the example prints all of the SI-defining constants: The main part of the example prints all of the SI-defining constants:
```cpp title="si_constants.cpp" linenums="17" ```cpp title="si_constants.cpp" linenums="14"
--8<-- "example/si_constants.cpp:45:" --8<-- "example/si_constants.cpp:42:"
``` ```
While analyzing the output of this program (provided below), we can easily notice that a direct While analyzing the output of this program (provided below), we can easily notice that a direct

View File

@@ -129,8 +129,8 @@ std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>&
template<typename T, typename Validator, typename Char> template<typename T, typename Validator, typename Char>
struct MP_UNITS_STD_FMT::formatter<validated_type<T, Validator>, Char> : formatter<T, Char> { struct MP_UNITS_STD_FMT::formatter<validated_type<T, Validator>, Char> : formatter<T, Char> {
template<typename FormatContext> template<typename FormatContext>
auto format(const validated_type<T, Validator>& v, FormatContext& ctx) const -> decltype(ctx.out()) auto format(const validated_type<T, Validator>& val, FormatContext& ctx) const -> decltype(ctx.out())
{ {
return formatter<T, Char>::format(v.value(), ctx); return formatter<T, Char>::format(val.value(), ctx);
} }
}; };

View File

@@ -39,10 +39,6 @@ import mp_units;
#include <mp-units/systems/si.h> #include <mp-units/systems/si.h>
#endif #endif
template<class T>
requires mp_units::is_scalar<T>
constexpr bool mp_units::is_vector<T> = true;
int main() int main()
{ {
using namespace mp_units; using namespace mp_units;

View File

@@ -91,6 +91,7 @@ if(NOT ${projectPrefix}API_FREESTANDING)
include/mp-units/bits/fmt.h include/mp-units/bits/fmt.h
include/mp-units/bits/requires_hosted.h include/mp-units/bits/requires_hosted.h
include/mp-units/ext/format.h include/mp-units/ext/format.h
include/mp-units/cartesian_vector.h
include/mp-units/complex.h include/mp-units/complex.h
include/mp-units/format.h include/mp-units/format.h
include/mp-units/math.h include/mp-units/math.h

View File

@@ -0,0 +1,230 @@
// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma once
#include <mp-units/bits/requires_hosted.h>
//
#include <mp-units/bits/module_macros.h>
#include <mp-units/framework/customization_points.h>
#if MP_UNITS_HOSTED
#include <mp-units/bits/fmt.h>
#endif
#ifndef MP_UNITS_IN_MODULE_INTERFACE
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <cmath>
#include <cstddef>
#include <type_traits>
#if MP_UNITS_HOSTED
#include <ostream>
#endif
#endif
#endif
namespace mp_units {
MP_UNITS_EXPORT template<typename T = double>
class cartesian_vector {
public:
// public members required to satisfy structural type requirements :-(
T _coordinates_[3];
using value_type = T;
cartesian_vector() = default;
cartesian_vector(const cartesian_vector&) = default;
cartesian_vector(cartesian_vector&&) = default;
cartesian_vector& operator=(const cartesian_vector&) = default;
cartesian_vector& operator=(cartesian_vector&&) = default;
template<std::convertible_to<T> Arg1, std::convertible_to<T>... Args>
constexpr cartesian_vector(Arg1&& arg1, Args&&... args) :
_coordinates_(std::forward<Arg1>(arg1), std::forward<Args>(args)...)
{
}
template<std::convertible_to<T> U>
constexpr cartesian_vector(const cartesian_vector<U>& other) : _coordinates_{other[0], other[1], other[2]}
{
}
template<std::convertible_to<T> U>
constexpr cartesian_vector(cartesian_vector<U>&& other) :
_coordinates_{std::move(other[0]), std::move(other[1]), std::move(other[2])}
{
}
template<std::convertible_to<T> U>
constexpr cartesian_vector& operator=(const cartesian_vector<U>& other)
{
_coordinates_[0] = other[0];
_coordinates_[1] = other[1];
_coordinates_[2] = other[2];
return *this;
}
template<std::convertible_to<T> U>
constexpr cartesian_vector& operator=(cartesian_vector<U>&& other)
{
_coordinates_[0] = std::move(other[0]);
_coordinates_[1] = std::move(other[1]);
_coordinates_[2] = std::move(other[2]);
return *this;
}
[[nodiscard]] constexpr T magnitude() const
requires treat_as_floating_point<T>
{
return std::hypot(_coordinates_[0], _coordinates_[1], _coordinates_[2]);
}
[[nodiscard]] constexpr cartesian_vector unit() const
requires treat_as_floating_point<T>
{
return *this / magnitude();
}
[[nodiscard]] constexpr T& operator[](std::size_t i) { return _coordinates_[i]; }
[[nodiscard]] constexpr const T& operator[](std::size_t i) const { return _coordinates_[i]; }
[[nodiscard]] constexpr cartesian_vector operator+() const { return *this; }
[[nodiscard]] constexpr cartesian_vector operator-() const
{
return {-_coordinates_[0], -_coordinates_[1], -_coordinates_[2]};
}
template<std::same_as<T> U, typename V>
requires requires(U u, V v) { u + v; }
[[nodiscard]] friend constexpr auto operator+(const cartesian_vector<U>& lhs, const cartesian_vector<V>& rhs)
{
return ::mp_units::cartesian_vector{lhs._coordinates_[0] + rhs._coordinates_[0],
lhs._coordinates_[1] + rhs._coordinates_[1],
lhs._coordinates_[2] + rhs._coordinates_[2]};
}
template<std::same_as<T> U, typename V>
requires requires(U u, V v) { u - v; }
[[nodiscard]] friend constexpr auto operator-(const cartesian_vector<U>& lhs, const cartesian_vector<V>& rhs)
{
return ::mp_units::cartesian_vector{lhs._coordinates_[0] - rhs._coordinates_[0],
lhs._coordinates_[1] - rhs._coordinates_[1],
lhs._coordinates_[2] - rhs._coordinates_[2]};
}
template<std::same_as<T> U>
requires requires(U u, T t) { u* t; }
[[nodiscard]] friend constexpr auto operator*(const cartesian_vector<U>& lhs, const T& rhs)
{
return ::mp_units::cartesian_vector{lhs._coordinates_[0] * rhs, lhs._coordinates_[1] * rhs,
lhs._coordinates_[2] * rhs};
}
template<std::same_as<T> U>
requires requires(T t, U u) { t* u; }
[[nodiscard]] friend constexpr auto operator*(const T& lhs, const cartesian_vector<U>& rhs)
{
return rhs * lhs;
}
template<std::same_as<T> U>
requires requires(U u, T t) { u / t; }
[[nodiscard]] friend constexpr auto operator/(const cartesian_vector<U>& lhs, const T& rhs)
{
return ::mp_units::cartesian_vector{lhs._coordinates_[0] / rhs, lhs._coordinates_[1] / rhs,
lhs._coordinates_[2] / rhs};
}
template<std::same_as<T> U, std::equality_comparable_with<U> V>
[[nodiscard]] friend constexpr bool operator==(const cartesian_vector<U>& lhs, const cartesian_vector<V>& rhs)
{
return lhs._coordinates_[0] == rhs._coordinates_[0] && lhs._coordinates_[1] == rhs._coordinates_[1] &&
lhs._coordinates_[2] == rhs._coordinates_[2];
}
[[nodiscard]] friend constexpr T norm(const cartesian_vector& vec)
requires treat_as_floating_point<T>
{
return vec.magnitude();
}
[[nodiscard]] friend constexpr cartesian_vector unit_vector(const cartesian_vector& vec)
requires treat_as_floating_point<T>
{
return vec.unit();
}
template<std::same_as<T> U, typename V>
requires requires(U u, V v, decltype(u * v) t) {
u* v;
t + t;
}
[[nodiscard]] friend constexpr auto scalar_product(const cartesian_vector<U>& lhs, const cartesian_vector<V>& rhs)
{
return lhs._coordinates_[0] * rhs._coordinates_[0] + lhs._coordinates_[1] * rhs._coordinates_[1] +
lhs._coordinates_[2] * rhs._coordinates_[2];
}
template<std::same_as<T> U, typename V>
requires requires(U u, V v, decltype(u * v) t) {
u* v;
t - t;
}
[[nodiscard]] friend constexpr auto vector_product(const cartesian_vector<U>& lhs, const cartesian_vector<V>& rhs)
{
return ::mp_units::cartesian_vector{
lhs._coordinates_[1] * rhs._coordinates_[2] - lhs._coordinates_[2] * rhs._coordinates_[1],
lhs._coordinates_[2] * rhs._coordinates_[0] - lhs._coordinates_[0] * rhs._coordinates_[2],
lhs._coordinates_[0] * rhs._coordinates_[1] - lhs._coordinates_[1] * rhs._coordinates_[0]};
}
#if MP_UNITS_HOSTED
friend constexpr std::ostream& operator<<(std::ostream& os, const cartesian_vector& vec)
{
return os << '[' << vec[0] << ", " << vec[1] << ", " << vec[2] << ']';
}
#endif
};
template<typename Arg, typename... Args>
requires(sizeof...(Args) <= 2) && requires { typename std::common_type_t<Arg, Args...>; }
cartesian_vector(Arg, Args...) -> cartesian_vector<std::common_type_t<Arg, Args...>>;
template<class T>
constexpr bool is_vector<cartesian_vector<T>> = true;
} // namespace mp_units
#if MP_UNITS_HOSTED
// TODO use parse and use formatter for the underlying type
template<typename T, typename Char>
struct MP_UNITS_STD_FMT::formatter<mp_units::cartesian_vector<T>, Char> :
formatter<std::basic_string_view<Char>, Char> {
template<typename FormatContext>
auto format(const mp_units::cartesian_vector<T>& vec, FormatContext& ctx) const
{
return format_to(ctx.out(), "[{}, {}, {}]", vec[0], vec[1], vec[2]);
}
};
#endif

View File

@@ -28,6 +28,7 @@
#include <mp-units/framework.h> #include <mp-units/framework.h>
#if MP_UNITS_HOSTED #if MP_UNITS_HOSTED
#include <mp-units/cartesian_vector.h>
#include <mp-units/complex.h> #include <mp-units/complex.h>
#include <mp-units/format.h> #include <mp-units/format.h>
#include <mp-units/math.h> #include <mp-units/math.h>

View File

@@ -90,16 +90,16 @@ public:
constexpr T* data() noexcept { return data_; } constexpr T* data() noexcept { return data_; }
constexpr const T* data() const noexcept { return data_; } constexpr const T* data() const noexcept { return data_; }
constexpr reference push_back(const T& v) constexpr reference push_back(const T& val)
requires std::constructible_from<T, const T&> requires std::constructible_from<T, const T&>
{ {
return emplace_back(v); return emplace_back(val);
} }
constexpr reference push_back(T&& v) constexpr reference push_back(T&& val)
requires std::constructible_from<T, T&&> requires std::constructible_from<T, T&&>
{ {
return emplace_back(std::forward<T&&>(v)); return emplace_back(std::forward<T&&>(val));
} }
template<typename... Args> template<typename... Args>

View File

@@ -174,21 +174,21 @@ public:
template<typename FwdValue, Reference R2> template<typename FwdValue, Reference R2>
requires detail::SameValueAs<R2{}, R, std::remove_cvref_t<FwdValue>, Rep> requires detail::SameValueAs<R2{}, R, std::remove_cvref_t<FwdValue>, Rep>
constexpr quantity(FwdValue&& v, R2) : numerical_value_is_an_implementation_detail_(std::forward<FwdValue>(v)) constexpr quantity(FwdValue&& val, R2) : numerical_value_is_an_implementation_detail_(std::forward<FwdValue>(val))
{ {
} }
template<typename FwdValue, Reference R2, typename Value = std::remove_cvref_t<FwdValue>> template<typename FwdValue, Reference R2, typename Value = std::remove_cvref_t<FwdValue>>
requires(!detail::SameValueAs<R2{}, R, Value, Rep>) && requires(!detail::SameValueAs<R2{}, R, Value, Rep>) &&
detail::QuantityConvertibleTo<quantity<R2{}, Value>, quantity> detail::QuantityConvertibleTo<quantity<R2{}, Value>, quantity>
constexpr quantity(FwdValue&& v, R2) : quantity(quantity<R2{}, Value>{std::forward<FwdValue>(v), R2{}}) constexpr quantity(FwdValue&& val, R2) : quantity(quantity<R2{}, Value>{std::forward<FwdValue>(val), R2{}})
{ {
} }
template<detail::ValuePreservingTo<Rep> FwdValue> template<detail::ValuePreservingTo<Rep> FwdValue>
requires(unit == ::mp_units::one) requires(unit == ::mp_units::one)
constexpr explicit(false) quantity(FwdValue&& v) : constexpr explicit(false) quantity(FwdValue&& val) :
numerical_value_is_an_implementation_detail_(std::forward<FwdValue>(v)) numerical_value_is_an_implementation_detail_(std::forward<FwdValue>(val))
{ {
} }
@@ -214,9 +214,9 @@ public:
template<detail::ValuePreservingTo<Rep> FwdValue> template<detail::ValuePreservingTo<Rep> FwdValue>
requires(unit == ::mp_units::one) requires(unit == ::mp_units::one)
constexpr quantity& operator=(FwdValue&& v) constexpr quantity& operator=(FwdValue&& val)
{ {
numerical_value_is_an_implementation_detail_ = std::forward<FwdValue>(v); numerical_value_is_an_implementation_detail_ = std::forward<FwdValue>(val);
return *this; return *this;
} }
@@ -422,11 +422,11 @@ public:
requires(!Quantity<Value>) && requires(rep a, Value b) { requires(!Quantity<Value>) && requires(rep a, Value b) {
{ a *= b } -> std::same_as<rep&>; { a *= b } -> std::same_as<rep&>;
} }
friend constexpr decltype(auto) operator*=(Q&& lhs, const Value& v) friend constexpr decltype(auto) operator*=(Q&& lhs, const Value& val)
{ {
// TODO use *= when compiler bug is resolved: // TODO use *= when compiler bug is resolved:
// https://developercommunity.visualstudio.com/t/Discrepancy-in-Behavior-of-operator-an/10732445 // https://developercommunity.visualstudio.com/t/Discrepancy-in-Behavior-of-operator-an/10732445
lhs.numerical_value_is_an_implementation_detail_ = lhs.numerical_value_is_an_implementation_detail_ * v; lhs.numerical_value_is_an_implementation_detail_ = lhs.numerical_value_is_an_implementation_detail_ * val;
return std::forward<Q>(lhs); return std::forward<Q>(lhs);
} }
@@ -444,12 +444,12 @@ public:
requires(!Quantity<Value>) && requires(rep a, Value b) { requires(!Quantity<Value>) && requires(rep a, Value b) {
{ a /= b } -> std::same_as<rep&>; { a /= b } -> std::same_as<rep&>;
} }
friend constexpr decltype(auto) operator/=(Q&& lhs, const Value& v) friend constexpr decltype(auto) operator/=(Q&& lhs, const Value& val)
{ {
MP_UNITS_EXPECTS_DEBUG(v != quantity_values<Value>::zero()); MP_UNITS_EXPECTS_DEBUG(val != quantity_values<Value>::zero());
// TODO use /= when compiler bug is resolved: // TODO use /= when compiler bug is resolved:
// https://developercommunity.visualstudio.com/t/Discrepancy-in-Behavior-of-operator-an/10732445 // https://developercommunity.visualstudio.com/t/Discrepancy-in-Behavior-of-operator-an/10732445
lhs.numerical_value_is_an_implementation_detail_ = lhs.numerical_value_is_an_implementation_detail_ / v; lhs.numerical_value_is_an_implementation_detail_ = lhs.numerical_value_is_an_implementation_detail_ / val;
return std::forward<Q>(lhs); return std::forward<Q>(lhs);
} }
@@ -551,17 +551,17 @@ public:
template<std::derived_from<quantity> Q, typename Value> template<std::derived_from<quantity> Q, typename Value>
requires(!Quantity<Value>) && requires(!Quantity<Value>) &&
(!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::multiplies<>, Rep, const Value&> (!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::multiplies<>, Rep, const Value&>
[[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator*(const Q& q, const Value& v) [[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator*(const Q& q, const Value& val)
{ {
return ::mp_units::quantity{q.numerical_value_ref_in(unit) * v, R}; return ::mp_units::quantity{q.numerical_value_ref_in(unit) * val, R};
} }
template<typename Value, std::derived_from<quantity> Q> template<typename Value, std::derived_from<quantity> Q>
requires(!Quantity<Value>) && requires(!Quantity<Value>) &&
(!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::multiplies<>, const Value&, Rep> (!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::multiplies<>, const Value&, Rep>
[[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator*(const Value& v, const Q& q) [[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator*(const Value& val, const Q& q)
{ {
return ::mp_units::quantity{v * q.numerical_value_ref_in(unit), R}; return ::mp_units::quantity{val * q.numerical_value_ref_in(unit), R};
} }
template<std::derived_from<quantity> Q, auto R2, typename Rep2> template<std::derived_from<quantity> Q, auto R2, typename Rep2>
@@ -575,18 +575,18 @@ public:
template<std::derived_from<quantity> Q, typename Value> template<std::derived_from<quantity> Q, typename Value>
requires(!Quantity<Value>) && requires(!Quantity<Value>) &&
(!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::divides<>, Rep, const Value&> (!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::divides<>, Rep, const Value&>
[[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator/(const Q& q, const Value& v) [[nodiscard]] friend constexpr QuantityOf<quantity_spec> auto operator/(const Q& q, const Value& val)
{ {
MP_UNITS_EXPECTS_DEBUG(v != quantity_values<Value>::zero()); MP_UNITS_EXPECTS_DEBUG(val != quantity_values<Value>::zero());
return ::mp_units::quantity{q.numerical_value_ref_in(unit) / v, R}; return ::mp_units::quantity{q.numerical_value_ref_in(unit) / val, R};
} }
template<typename Value, std::derived_from<quantity> Q> template<typename Value, std::derived_from<quantity> Q>
requires(!Quantity<Value>) && requires(!Quantity<Value>) &&
(!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::divides<>, const Value&, Rep> (!Reference<Value>) && detail::InvokeResultOf<quantity_spec, std::divides<>, const Value&, Rep>
[[nodiscard]] friend constexpr QuantityOf<inverse(quantity_spec)> auto operator/(const Value& v, const Q& q) [[nodiscard]] friend constexpr QuantityOf<inverse(quantity_spec)> auto operator/(const Value& val, const Q& q)
{ {
return ::mp_units::quantity{v / q.numerical_value_ref_in(unit), ::mp_units::one / R}; return ::mp_units::quantity{val / q.numerical_value_ref_in(unit), ::mp_units::one / R};
} }
template<std::derived_from<quantity> Q, auto R2, typename Rep2> template<std::derived_from<quantity> Q, auto R2, typename Rep2>

View File

@@ -59,7 +59,7 @@ template<QuantitySpec QS, Unit U>
requires(!AssociatedUnit<U>) || UnitOf<U, QS{}> requires(!AssociatedUnit<U>) || UnitOf<U, QS{}>
[[nodiscard]] consteval Reference auto make_reference(QS, U u) [[nodiscard]] consteval Reference auto make_reference(QS, U u)
{ {
if constexpr (QuantityKindSpec<QS>) if constexpr (requires { requires(get_quantity_spec(U{}) == QS{}); })
return u; return u;
else else
return reference<QS, U>{}; return reference<QS, U>{};

View File

@@ -118,12 +118,11 @@ concept ComplexRepresentation = Complex<T> && WeaklyRegular<T> && requires(T a,
{ a - b } -> Complex; { a - b } -> Complex;
{ a* b } -> Complex; { a* b } -> Complex;
{ a / b } -> Complex; { a / b } -> Complex;
// TBD { real(a) } -> Scalar;
// { re(a) } -> Scalar; { imag(a) } -> Scalar;
// { im(a) } -> Scalar; { abs(a) } -> Scalar;
// { mod(a) } -> Scalar; { arg(a) } -> Scalar;
// { arg(a) } -> Scalar; { conj(a) } -> Complex;
// { conj(a) } -> Complex;
}; };
// TODO how to check for a complex(Scalar, Scalar) -> Complex? // TODO how to check for a complex(Scalar, Scalar) -> Complex?

View File

@@ -68,8 +68,8 @@ void to_stream_impl(std::basic_ostream<CharT, Traits>& os, const quantity<R, Rep
} }
template<typename CharT, class Traits, typename T> template<typename CharT, class Traits, typename T>
std::basic_ostream<CharT, Traits>& to_stream(std::basic_ostream<CharT, Traits>& os, const T& v) std::basic_ostream<CharT, Traits>& to_stream(std::basic_ostream<CharT, Traits>& os, const T& val)
requires requires { detail::to_stream_impl(os, v); } requires requires { detail::to_stream_impl(os, val); }
{ {
if (os.width()) { if (os.width()) {
// std::setw() applies to the whole output so it has to be first put into std::string // std::setw() applies to the whole output so it has to be first put into std::string
@@ -77,11 +77,11 @@ std::basic_ostream<CharT, Traits>& to_stream(std::basic_ostream<CharT, Traits>&
oss.flags(os.flags()); oss.flags(os.flags());
oss.imbue(os.getloc()); oss.imbue(os.getloc());
oss.precision(os.precision()); oss.precision(os.precision());
detail::to_stream_impl(oss, v); detail::to_stream_impl(oss, val);
return os << std::move(oss).str(); return os << std::move(oss).str();
} }
detail::to_stream_impl(os, v); detail::to_stream_impl(os, val);
return os; return os;
} }
@@ -93,10 +93,10 @@ constexpr bool is_mp_units_stream = requires(OStream os, T v) { detail::to_strea
MP_UNITS_EXPORT_BEGIN MP_UNITS_EXPORT_BEGIN
template<typename CharT, typename Traits, typename T> template<typename CharT, typename Traits, typename T>
std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>& os, const T& v) std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>& os, const T& val)
requires detail::is_mp_units_stream<std::basic_ostream<CharT, Traits>, T> requires detail::is_mp_units_stream<std::basic_ostream<CharT, Traits>, T>
{ {
return detail::to_stream(os, v); return detail::to_stream(os, val);
} }
MP_UNITS_EXPORT_END MP_UNITS_EXPORT_END

View File

@@ -83,10 +83,10 @@ struct quantity_like_traits<std::chrono::duration<Rep, Period>> {
return q.count(); return q.count();
} }
[[nodiscard]] static constexpr T from_numerical_value(const rep& v) noexcept( [[nodiscard]] static constexpr T from_numerical_value(const rep& val) noexcept(
std::is_nothrow_copy_constructible_v<rep>) std::is_nothrow_copy_constructible_v<rep>)
{ {
return T(v); return T(val);
} }
}; };
@@ -113,10 +113,10 @@ struct quantity_point_like_traits<std::chrono::time_point<C, std::chrono::durati
return tp.time_since_epoch().count(); return tp.time_since_epoch().count();
} }
[[nodiscard]] static constexpr T from_numerical_value(const rep& v) noexcept( [[nodiscard]] static constexpr T from_numerical_value(const rep& val) noexcept(
std::is_nothrow_copy_constructible_v<rep>) std::is_nothrow_copy_constructible_v<rep>)
{ {
return T(std::chrono::duration<Rep, Period>(v)); return T(std::chrono::duration<Rep, Period>(val));
} }
}; };

View File

@@ -24,13 +24,14 @@ find_package(Catch2 3 REQUIRED)
add_executable( add_executable(
unit_tests_runtime unit_tests_runtime
atomic_test.cpp
cartesian_vector_test.cpp
distribution_test.cpp distribution_test.cpp
fixed_string_test.cpp fixed_string_test.cpp
fmt_test.cpp fmt_test.cpp
math_test.cpp math_test.cpp
atomic_test.cpp
truncation_test.cpp
quantity_test.cpp quantity_test.cpp
truncation_test.cpp
) )
if(${projectPrefix}BUILD_CXX_MODULES) if(${projectPrefix}BUILD_CXX_MODULES)
target_compile_definitions(unit_tests_runtime PUBLIC ${projectPrefix}MODULES) target_compile_definitions(unit_tests_runtime PUBLIC ${projectPrefix}MODULES)

View File

@@ -0,0 +1,381 @@
// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include "almost_equals.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <mp-units/compat_macros.h>
#include <mp-units/ext/format.h>
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <sstream>
#endif
#ifdef MP_UNITS_MODULES
import mp_units;
#else
#include <mp-units/cartesian_vector.h>
#endif
using namespace mp_units;
using namespace Catch::Matchers;
TEST_CASE("cartesian_vector operations", "[vector]")
{
SECTION("cartesian_vector initialization and access")
{
SECTION("one argument")
{
cartesian_vector v{1.0};
REQUIRE(v[0] == 1.0);
REQUIRE(v[1] == 0);
REQUIRE(v[2] == 0);
}
SECTION("two arguments")
{
cartesian_vector v{1.0, 2.0};
REQUIRE(v[0] == 1.0);
REQUIRE(v[1] == 2.0);
REQUIRE(v[2] == 0);
}
SECTION("all arguments")
{
cartesian_vector v{1.0, 2.0, 3.0};
REQUIRE(v[0] == 1.0);
REQUIRE(v[1] == 2.0);
REQUIRE(v[2] == 3.0);
}
}
SECTION("convertibility")
{
cartesian_vector v1{1, 2, 3};
SECTION("construction")
{
cartesian_vector v2 = v1;
REQUIRE(v2[0] == 1.0);
REQUIRE(v2[1] == 2.0);
REQUIRE(v2[2] == 3.0);
}
SECTION("assignment")
{
cartesian_vector v2{3.0, 2.0, 1.0};
v2 = v1;
REQUIRE(v2[0] == 1.0);
REQUIRE(v2[1] == 2.0);
REQUIRE(v2[2] == 3.0);
}
}
SECTION("cartesian_vector addition")
{
SECTION("double + double")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4.0, 5.0, 6.0};
cartesian_vector result = v1 + v2;
REQUIRE(result[0] == 5.0);
REQUIRE(result[1] == 7.0);
REQUIRE(result[2] == 9.0);
}
SECTION("double + int")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4, 5, 6};
cartesian_vector result = v1 + v2;
REQUIRE(result[0] == 5.0);
REQUIRE(result[1] == 7.0);
REQUIRE(result[2] == 9.0);
}
SECTION("int + double")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4.0, 5.0, 6.0};
cartesian_vector result = v1 + v2;
REQUIRE(result[0] == 5.0);
REQUIRE(result[1] == 7.0);
REQUIRE(result[2] == 9.0);
}
SECTION("int + int")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4, 5, 6};
cartesian_vector result = v1 + v2;
REQUIRE(result[0] == 5);
REQUIRE(result[1] == 7);
REQUIRE(result[2] == 9);
}
}
SECTION("cartesian_vector subtraction")
{
SECTION("double - double")
{
cartesian_vector v1{4.0, 5.0, 6.0};
cartesian_vector v2{1.0, 2.0, 3.0};
cartesian_vector result = v1 - v2;
REQUIRE(result[0] == 3.0);
REQUIRE(result[1] == 3.0);
REQUIRE(result[2] == 3.0);
}
SECTION("double - int")
{
cartesian_vector v1{4.0, 5.0, 6.0};
cartesian_vector v2{1, 2, 3};
cartesian_vector result = v1 - v2;
REQUIRE(result[0] == 3.0);
REQUIRE(result[1] == 3.0);
REQUIRE(result[2] == 3.0);
}
SECTION("int - double")
{
cartesian_vector v1{4, 5, 6};
cartesian_vector v2{1.0, 2.0, 3.0};
cartesian_vector result = v1 - v2;
REQUIRE(result[0] == 3.0);
REQUIRE(result[1] == 3.0);
REQUIRE(result[2] == 3.0);
}
SECTION("int - int")
{
cartesian_vector v1{4, 5, 6};
cartesian_vector v2{1, 2, 3};
cartesian_vector result = v1 - v2;
REQUIRE(result[0] == 3);
REQUIRE(result[1] == 3);
REQUIRE(result[2] == 3);
}
}
SECTION("cartesian_vector scalar multiplication")
{
SECTION("double * double")
{
cartesian_vector v{1.0, 2.0, 3.0};
cartesian_vector result = v * 2.0;
REQUIRE(result[0] == 2.0);
REQUIRE(result[1] == 4.0);
REQUIRE(result[2] == 6.0);
}
SECTION("double * int")
{
cartesian_vector v{1.0, 2.0, 3.0};
cartesian_vector result = v * 2;
REQUIRE(result[0] == 2.0);
REQUIRE(result[1] == 4.0);
REQUIRE(result[2] == 6.0);
}
SECTION("int * double")
{
cartesian_vector v{1, 2, 3};
cartesian_vector result = v * 2.0;
REQUIRE(result[0] == 2.0);
REQUIRE(result[1] == 4.0);
REQUIRE(result[2] == 6.0);
}
SECTION("int * int")
{
cartesian_vector v{1, 2, 3};
cartesian_vector result = v * 2;
REQUIRE(result[0] == 2);
REQUIRE(result[1] == 4);
REQUIRE(result[2] == 6);
}
}
SECTION("cartesian_vector scalar division")
{
SECTION("double / double")
{
cartesian_vector v{2.0, 4.0, 6.0};
cartesian_vector result = v / 2.0;
REQUIRE(result[0] == 1.0);
REQUIRE(result[1] == 2.0);
REQUIRE(result[2] == 3.0);
}
SECTION("double / int")
{
cartesian_vector v{2.0, 4.0, 6.0};
cartesian_vector result = v / 2;
REQUIRE(result[0] == 1.0);
REQUIRE(result[1] == 2.0);
REQUIRE(result[2] == 3.0);
}
SECTION("int / double")
{
cartesian_vector v{2, 4, 6};
cartesian_vector result = v / 2.0;
REQUIRE(result[0] == 1.0);
REQUIRE(result[1] == 2.0);
REQUIRE(result[2] == 3.0);
}
SECTION("int / int")
{
cartesian_vector v{2, 4, 6};
cartesian_vector result = v / 2;
REQUIRE(result[0] == 1);
REQUIRE(result[1] == 2);
REQUIRE(result[2] == 3);
}
}
SECTION("cartesian_vector magnitude")
{
cartesian_vector v1{3.0, 4.0, 0.0};
cartesian_vector v2{2.0, 3.0, 6.0};
REQUIRE(v1.magnitude() == 5.0);
REQUIRE(v2.magnitude() == 7.0);
}
SECTION("cartesian_vector unit vector")
{
cartesian_vector v{3.0, 4.0, 0.0};
cartesian_vector unit_v = v.unit();
REQUIRE_THAT(unit_v.magnitude(), WithinULP(1.0, 1));
}
SECTION("cartesian_vector equality")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{1, 2, 3};
cartesian_vector v3{1.1, 2.0, 3.0};
cartesian_vector v4{1.0, 2.1, 3.0};
cartesian_vector v5{1.0, 2.0, 3.1};
REQUIRE(v1 == v2);
REQUIRE(v1 != v3);
REQUIRE(v1 != v4);
REQUIRE(v1 != v5);
}
SECTION("cartesian_vector scalar product")
{
SECTION("double * double")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4.0, 5.0, 6.0};
REQUIRE(scalar_product(v1, v2) == 32.0);
}
SECTION("double * int")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4, 5, 6};
REQUIRE(scalar_product(v1, v2) == 32.0);
}
SECTION("int * double")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4.0, 5.0, 6.0};
REQUIRE(scalar_product(v1, v2) == 32.0);
}
SECTION("int * int")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4, 5, 6};
REQUIRE(scalar_product(v1, v2) == 32);
}
}
SECTION("cartesian_vector vector product")
{
SECTION("double * double")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4.0, 5.0, 6.0};
cartesian_vector result = vector_product(v1, v2);
REQUIRE(result[0] == -3.0);
REQUIRE(result[1] == 6.0);
REQUIRE(result[2] == -3.0);
}
SECTION("double * int")
{
cartesian_vector v1{1.0, 2.0, 3.0};
cartesian_vector v2{4, 5, 6};
cartesian_vector result = vector_product(v1, v2);
REQUIRE(result[0] == -3.0);
REQUIRE(result[1] == 6.0);
REQUIRE(result[2] == -3.0);
}
SECTION("int * double")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4.0, 5.0, 6.0};
cartesian_vector result = vector_product(v1, v2);
REQUIRE(result[0] == -3.0);
REQUIRE(result[1] == 6.0);
REQUIRE(result[2] == -3.0);
}
SECTION("int * int")
{
cartesian_vector v1{1, 2, 3};
cartesian_vector v2{4, 5, 6};
cartesian_vector result = vector_product(v1, v2);
REQUIRE(result[0] == -3);
REQUIRE(result[1] == 6);
REQUIRE(result[2] == -3);
}
}
}
TEST_CASE("cartesian_vector text output", "[vector][fmt][ostream]")
{
std::ostringstream os;
SECTION("integral representation")
{
cartesian_vector v{1, 2, 3};
os << v;
SECTION("iostream") { CHECK(os.str() == "[1, 2, 3]"); }
SECTION("fmt with default format {}") { CHECK(MP_UNITS_STD_FMT::format("{}", v) == os.str()); }
}
SECTION("floating-point representation")
{
cartesian_vector v{1.2, 2.3, 3.4};
os << v;
SECTION("iostream") { CHECK(os.str() == "[1.2, 2.3, 3.4]"); }
SECTION("fmt with default format {}") { CHECK(MP_UNITS_STD_FMT::format("{}", v) == os.str()); }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -38,19 +38,22 @@ import mp_units;
using namespace mp_units; using namespace mp_units;
TEST_CASE("fixed_string::at", "[fixed_string]") TEST_CASE("fixed_string operations", "[fixed_string]")
{ {
basic_fixed_string txt = "abc"; SECTION("fixed_string::at")
SECTION("in range")
{ {
CHECK(txt.at(0) == 'a'); basic_fixed_string txt = "abc";
CHECK(txt.at(1) == 'b'); SECTION("in range")
CHECK(txt.at(2) == 'c'); {
} CHECK(txt.at(0) == 'a');
SECTION("out of range") CHECK(txt.at(1) == 'b');
{ CHECK(txt.at(2) == 'c');
REQUIRE_THROWS_MATCHES(txt.at(3), std::out_of_range, Catch::Matchers::Message("basic_fixed_string::at")); }
REQUIRE_THROWS_MATCHES(txt.at(1024), std::out_of_range, Catch::Matchers::Message("basic_fixed_string::at")); SECTION("out of range")
{
REQUIRE_THROWS_MATCHES(txt.at(3), std::out_of_range, Catch::Matchers::Message("basic_fixed_string::at"));
REQUIRE_THROWS_MATCHES(txt.at(1024), std::out_of_range, Catch::Matchers::Message("basic_fixed_string::at"));
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -43,486 +43,488 @@ using namespace mp_units::si::unit_symbols;
// classical // classical
TEST_CASE("'pow<N>()' on quantity changes the value and the dimension accordingly", "[math][pow]") TEST_CASE("math operations", "[math]")
{ {
SECTION("'pow<0>(q)' returns '1'") { CHECK(pow<0>(2 * isq::length[m]) == 1 * one); } SECTION("'pow<N>()' on quantity changes the value and the dimension accordingly")
SECTION("'pow<1>(q)' returns 'q'") { CHECK(pow<1>(2 * isq::length[m]) == 2 * isq::length[m]); }
SECTION("'pow<2>(q)' squares both the value and a dimension")
{ {
CHECK(pow<2>(2 * isq::length[m]) == 4 * isq::area[m2]); SECTION("'pow<0>(q)' returns '1'") { CHECK(pow<0>(2 * isq::length[m]) == 1 * one); }
SECTION("'pow<1>(q)' returns 'q'") { CHECK(pow<1>(2 * isq::length[m]) == 2 * isq::length[m]); }
SECTION("'pow<2>(q)' squares both the value and a dimension")
{
CHECK(pow<2>(2 * isq::length[m]) == 4 * isq::area[m2]);
}
SECTION("'pow<3>(q)' cubes both the value and a dimension")
{
CHECK(pow<3>(2 * isq::length[m]) == 8 * isq::volume[m3]);
}
} }
SECTION("'pow<3>(q)' cubes both the value and a dimension") SECTION("'sqrt()' on quantity changes the value and the dimension accordingly")
{ {
CHECK(pow<3>(2 * isq::length[m]) == 8 * isq::volume[m3]); REQUIRE(sqrt(4 * isq::area[m2]) == 2 * isq::length[m]);
} }
}
SECTION("'cbrt()' on quantity changes the value and the dimension accordingly")
TEST_CASE("'sqrt()' on quantity changes the value and the dimension accordingly", "[math][sqrt]") {
{ REQUIRE(cbrt(8 * isq::volume[m3]) == 2 * isq::length[m]);
REQUIRE(sqrt(4 * isq::area[m2]) == 2 * isq::length[m]); }
}
SECTION("'fma()' on quantity changes the value and the dimension accordingly")
TEST_CASE("'cbrt()' on quantity changes the value and the dimension accordingly", "[math][cbrt]") {
{ REQUIRE(fma(1.0 * isq::length[m], 2.0 * one, 2.0 * isq::length[m]) == 4.0 * isq::length[m]);
REQUIRE(cbrt(8 * isq::volume[m3]) == 2 * isq::length[m]); REQUIRE(fma(isq::speed(10.0 * m / s), isq::time(2.0 * s), isq::height(42.0 * m)) == isq::length(62.0 * m));
} }
TEST_CASE("'fma()' on quantity changes the value and the dimension accordingly", "[math][fma]") SECTION("fmod functions")
{ {
REQUIRE(fma(1.0 * isq::length[m], 2.0 * one, 2.0 * isq::length[m]) == 4.0 * isq::length[m]); SECTION("fmod should work on the same quantities")
REQUIRE(fma(isq::speed(10.0 * m / s), isq::time(2.0 * s), isq::height(42.0 * m)) == isq::length(62.0 * m)); {
} REQUIRE(fmod(4. * isq::length[km], 3. * isq::length[km]) == 1. * isq::length[km]);
REQUIRE(fmod(-9. * isq::length[km], 3. * isq::length[km]) == -0. * isq::length[km]);
TEST_CASE("fmod functions", "[math][fmod]") REQUIRE(fmod(3 * isq::length[km], 2 * isq::length[km]) == 1 * isq::length[km]);
{ REQUIRE(fmod(4 * isq::length[km], 2.5f * isq::length[km]) == 1.5 * isq::length[km]);
SECTION("fmod should work on the same quantities") }
{ SECTION("fmod should work with different units of the same dimension")
REQUIRE(fmod(4. * isq::length[km], 3. * isq::length[km]) == 1. * isq::length[km]); {
REQUIRE(fmod(-9. * isq::length[km], 3. * isq::length[km]) == -0. * isq::length[km]); REQUIRE(fmod(4. * isq::length[km], 3000. * isq::length[m]) == 1000. * isq::length[m]);
REQUIRE(fmod(3 * isq::length[km], 2 * isq::length[km]) == 1 * isq::length[km]); REQUIRE(fmod(-9. * isq::length[km], 3000. * isq::length[m]) == -0. * isq::length[m]);
REQUIRE(fmod(4 * isq::length[km], 2.5f * isq::length[km]) == 1.5 * isq::length[km]); REQUIRE(fmod(3. * isq::length[km], 2000. * isq::length[m]) == 1000 * isq::length[m]);
} REQUIRE(fmod(4 * isq::length[km], 2500 * isq::length[m]) == 1500 * isq::length[m]);
SECTION("fmod should work with different units of the same dimension") }
{ }
REQUIRE(fmod(4. * isq::length[km], 3000. * isq::length[m]) == 1000. * isq::length[m]);
REQUIRE(fmod(-9. * isq::length[km], 3000. * isq::length[m]) == -0. * isq::length[m]); SECTION("remainder functions")
REQUIRE(fmod(3. * isq::length[km], 2000. * isq::length[m]) == 1000 * isq::length[m]); {
REQUIRE(fmod(4 * isq::length[km], 2500 * isq::length[m]) == 1500 * isq::length[m]); SECTION("remainder should work on the same quantities")
} {
} REQUIRE(remainder(4. * isq::length[km], 3. * isq::length[km]) == 1. * isq::length[km]);
REQUIRE(remainder(-9. * isq::length[km], 3. * isq::length[km]) == -0. * isq::length[km]);
TEST_CASE("remainder functions", "[math][remainder]") REQUIRE(remainder(3 * isq::length[km], 2 * isq::length[km]) == -1 * isq::length[km]);
{ REQUIRE(remainder(4 * isq::length[km], 2.75f * isq::length[km]) == 1.25 * isq::length[km]);
SECTION("remainder should work on the same quantities") }
{ SECTION("remainder should work with different units of the same dimension")
REQUIRE(remainder(4. * isq::length[km], 3. * isq::length[km]) == 1. * isq::length[km]); {
REQUIRE(remainder(-9. * isq::length[km], 3. * isq::length[km]) == -0. * isq::length[km]); REQUIRE(remainder(4. * isq::length[km], 3000. * isq::length[m]) == 1000. * isq::length[m]);
REQUIRE(remainder(3 * isq::length[km], 2 * isq::length[km]) == -1 * isq::length[km]); REQUIRE(remainder(-9. * isq::length[km], 3000. * isq::length[m]) == -0. * isq::length[m]);
REQUIRE(remainder(4 * isq::length[km], 2.75f * isq::length[km]) == 1.25 * isq::length[km]); REQUIRE(remainder(3. * isq::length[km], 2000. * isq::length[m]) == -1000 * isq::length[m]);
} REQUIRE(remainder(4 * isq::length[km], 2750 * isq::length[m]) == 1250 * isq::length[m]);
SECTION("remainder should work with different units of the same dimension") }
{ }
REQUIRE(remainder(4. * isq::length[km], 3000. * isq::length[m]) == 1000. * isq::length[m]);
REQUIRE(remainder(-9. * isq::length[km], 3000. * isq::length[m]) == -0. * isq::length[m]); SECTION("'isfinite()' accepts dimensioned arguments") { REQUIRE(isfinite(4.0 * isq::length[m])); }
REQUIRE(remainder(3. * isq::length[km], 2000. * isq::length[m]) == -1000 * isq::length[m]);
REQUIRE(remainder(4 * isq::length[km], 2750 * isq::length[m]) == 1250 * isq::length[m]); SECTION("'isinf()' accepts dimensioned arguments") { REQUIRE(!isinf(4.0 * isq::length[m])); }
}
} SECTION("'isnan()' accepts dimensioned arguments") { REQUIRE(!isnan(4.0 * isq::length[m])); }
TEST_CASE("'isfinite()' accepts dimensioned arguments", "[math][isfinite]") { REQUIRE(isfinite(4.0 * isq::length[m])); }
SECTION("'pow<Num, Den>()' on quantity changes the value and the dimension accordingly")
TEST_CASE("'isinf()' accepts dimensioned arguments", "[math][isinf]") { REQUIRE(!isinf(4.0 * isq::length[m])); } {
REQUIRE(pow<1, 4>(16 * isq::area[m2]) == sqrt(4 * isq::length[m]));
TEST_CASE("'isnan()' accepts dimensioned arguments", "[math][isnan]") { REQUIRE(!isnan(4.0 * isq::length[m])); } }
// TODO add tests for exp()
TEST_CASE("'pow<Num, Den>()' on quantity changes the value and the dimension accordingly", "[math][pow]")
{ SECTION("absolute functions on quantity returns the absolute value")
REQUIRE(pow<1, 4>(16 * isq::area[m2]) == sqrt(4 * isq::length[m])); {
} SECTION("'abs()' on a negative quantity returns the abs")
{
// TODO add tests for exp() SECTION("integral representation") { REQUIRE(abs(-1 * isq::length[m]) == 1 * isq::length[m]); }
TEST_CASE("absolute functions on quantity returns the absolute value", "[math][abs][fabs]") SECTION("floating-point representation") { REQUIRE(abs(-1. * isq::length[m]) == 1 * isq::length[m]); }
{ }
SECTION("'abs()' on a negative quantity returns the abs")
{ SECTION("'abs()' on a positive quantity returns the abs")
SECTION("integral representation") { REQUIRE(abs(-1 * isq::length[m]) == 1 * isq::length[m]); } {
SECTION("integral representation") { REQUIRE(abs(1 * isq::length[m]) == 1 * isq::length[m]); }
SECTION("floating-point representation") { REQUIRE(abs(-1. * isq::length[m]) == 1 * isq::length[m]); }
} SECTION("floating-point representation") { REQUIRE(abs(1. * isq::length[m]) == 1 * isq::length[m]); }
}
SECTION("'abs()' on a positive quantity returns the abs") }
{
SECTION("integral representation") { REQUIRE(abs(1 * isq::length[m]) == 1 * isq::length[m]); } SECTION("numeric_limits functions")
{
SECTION("floating-point representation") { REQUIRE(abs(1. * isq::length[m]) == 1 * isq::length[m]); } SECTION("'epsilon' works as expected using default floating type")
} {
} REQUIRE(epsilon<double>(isq::length[m]).numerical_value_in(m) ==
std::numeric_limits<decltype(1. * isq::length[m])::rep>::epsilon());
TEST_CASE("numeric_limits functions", "[limits]") }
{ SECTION("'epsilon' works as expected using integers")
SECTION("'epsilon' works as expected using default floating type") {
{ REQUIRE(epsilon<int>(isq::length[m]).numerical_value_in(m) ==
REQUIRE(epsilon<double>(isq::length[m]).numerical_value_in(m) == std::numeric_limits<decltype(1 * isq::length[m])::rep>::epsilon());
std::numeric_limits<decltype(1. * isq::length[m])::rep>::epsilon()); }
} }
SECTION("'epsilon' works as expected using integers")
{ SECTION("floor functions")
REQUIRE(epsilon<int>(isq::length[m]).numerical_value_in(m) == {
std::numeric_limits<decltype(1 * isq::length[m])::rep>::epsilon()); SECTION("floor 1 second with target unit second should be 1 second")
} {
} REQUIRE(floor<si::second>(1 * isq::time[s]) == 1 * isq::time[s]);
}
TEST_CASE("floor functions", "[floor]") SECTION("floor 1000 milliseconds with target unit second should be 1 second")
{ {
SECTION("floor 1 second with target unit second should be 1 second") REQUIRE(floor<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1 * isq::time[s]) == 1 * isq::time[s]); SECTION("floor 1001 milliseconds with target unit second should be 1 second")
} {
SECTION("floor 1000 milliseconds with target unit second should be 1 second") REQUIRE(floor<si::second>(1001 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]); SECTION("floor 1999 milliseconds with target unit second should be 1 second")
} {
SECTION("floor 1001 milliseconds with target unit second should be 1 second") REQUIRE(floor<si::second>(1999 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1001 * isq::time[ms]) == 1 * isq::time[s]); SECTION("floor -1000 milliseconds with target unit second should be -1 second")
} {
SECTION("floor 1999 milliseconds with target unit second should be 1 second") REQUIRE(floor<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1999 * isq::time[ms]) == 1 * isq::time[s]); SECTION("floor -999 milliseconds with target unit second should be -1 second")
} {
SECTION("floor -1000 milliseconds with target unit second should be -1 second") REQUIRE(floor<si::second>(-999 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]); SECTION("floor 1.3 seconds with target unit second should be 1 second")
} {
SECTION("floor -999 milliseconds with target unit second should be -1 second") REQUIRE(floor<si::second>(1.3 * isq::time[s]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(-999 * isq::time[ms]) == -1 * isq::time[s]); SECTION("floor -1.3 seconds with target unit second should be -2 seconds")
} {
SECTION("floor 1.3 seconds with target unit second should be 1 second") REQUIRE(floor<si::second>(-1.3 * isq::time[s]) == -2 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1.3 * isq::time[s]) == 1 * isq::time[s]); SECTION("floor 1001. milliseconds with target unit second should be 1 second")
} {
SECTION("floor -1.3 seconds with target unit second should be -2 seconds") REQUIRE(floor<si::second>(1001. * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(-1.3 * isq::time[s]) == -2 * isq::time[s]); SECTION("floor 1999. milliseconds with target unit second should be 1 second")
} {
SECTION("floor 1001. milliseconds with target unit second should be 1 second") REQUIRE(floor<si::second>(1999. * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1001. * isq::time[ms]) == 1 * isq::time[s]); SECTION("floor -1000. milliseconds with target unit second should be -1 second")
} {
SECTION("floor 1999. milliseconds with target unit second should be 1 second") REQUIRE(floor<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(1999. * isq::time[ms]) == 1 * isq::time[s]); SECTION("floor -999. milliseconds with target unit second should be -1 second")
} {
SECTION("floor -1000. milliseconds with target unit second should be -1 second") REQUIRE(floor<si::second>(-999. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(floor<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]);
} // TODO Add tests for `N`, `kN` and `kg * m / s2` i `kg * km / s2`
SECTION("floor -999. milliseconds with target unit second should be -1 second") }
{
REQUIRE(floor<si::second>(-999. * isq::time[ms]) == -1 * isq::time[s]); SECTION("ceil functions")
} {
SECTION("ceil 1 second with target unit second should be 1 second")
// TODO Add tests for `N`, `kN` and `kg * m / s2` i `kg * km / s2` {
} REQUIRE(ceil<si::second>(1 * isq::time[s]) == 1 * isq::time[s]);
}
TEST_CASE("ceil functions", "[ceil]") SECTION("ceil 1000 milliseconds with target unit second should be 1 second")
{ {
SECTION("ceil 1 second with target unit second should be 1 second") REQUIRE(ceil<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1 * isq::time[s]) == 1 * isq::time[s]); SECTION("ceil 1001 milliseconds with target unit second should be 2 seconds")
} {
SECTION("ceil 1000 milliseconds with target unit second should be 1 second") REQUIRE(ceil<si::second>(1001 * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]); SECTION("ceil 1999 milliseconds with target unit second should be 2 seconds")
} {
SECTION("ceil 1001 milliseconds with target unit second should be 2 seconds") REQUIRE(ceil<si::second>(1999 * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1001 * isq::time[ms]) == 2 * isq::time[s]); SECTION("ceil -1000 milliseconds with target unit second should be -1 second")
} {
SECTION("ceil 1999 milliseconds with target unit second should be 2 seconds") REQUIRE(ceil<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1999 * isq::time[ms]) == 2 * isq::time[s]); SECTION("ceil -999 milliseconds with target unit second should be 0 seconds")
} {
SECTION("ceil -1000 milliseconds with target unit second should be -1 second") REQUIRE(ceil<si::second>(-999 * isq::time[ms]) == 0 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]); SECTION("ceil 1.3 seconds with target unit second should be 2 seconds")
} {
SECTION("ceil -999 milliseconds with target unit second should be 0 seconds") REQUIRE(ceil<si::second>(1.3 * isq::time[s]) == 2 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(-999 * isq::time[ms]) == 0 * isq::time[s]); SECTION("ceil -1.3 seconds with target unit second should be -1 second")
} {
SECTION("ceil 1.3 seconds with target unit second should be 2 seconds") REQUIRE(ceil<si::second>(-1.3 * isq::time[s]) == -1 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1.3 * isq::time[s]) == 2 * isq::time[s]); SECTION("ceil 1001. milliseconds with target unit second should be 2 seconds")
} {
SECTION("ceil -1.3 seconds with target unit second should be -1 second") REQUIRE(ceil<si::second>(1001. * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(-1.3 * isq::time[s]) == -1 * isq::time[s]); SECTION("ceil 1999. milliseconds with target unit second should be 2 seconds")
} {
SECTION("ceil 1001. milliseconds with target unit second should be 2 seconds") REQUIRE(ceil<si::second>(1999. * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1001. * isq::time[ms]) == 2 * isq::time[s]); SECTION("ceil -1000. milliseconds with target unit second should be -1 second")
} {
SECTION("ceil 1999. milliseconds with target unit second should be 2 seconds") REQUIRE(ceil<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(1999. * isq::time[ms]) == 2 * isq::time[s]); SECTION("ceil -999. milliseconds with target unit second should be 0 seconds")
} {
SECTION("ceil -1000. milliseconds with target unit second should be -1 second") REQUIRE(ceil<si::second>(-999. * isq::time[ms]) == 0 * isq::time[s]);
{ }
REQUIRE(ceil<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]); }
}
SECTION("ceil -999. milliseconds with target unit second should be 0 seconds") SECTION("round functions")
{ {
REQUIRE(ceil<si::second>(-999. * isq::time[ms]) == 0 * isq::time[s]); SECTION("round 1 second with target unit second should be 1 second")
} {
} REQUIRE(round<si::second>(1 * isq::time[s]) == 1 * isq::time[s]);
}
TEST_CASE("round functions", "[round]") SECTION("round 1000 milliseconds with target unit second should be 1 second")
{ {
SECTION("round 1 second with target unit second should be 1 second") REQUIRE(round<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1 * isq::time[s]) == 1 * isq::time[s]); SECTION("round 1001 milliseconds with target unit second should be 1 second")
} {
SECTION("round 1000 milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1001 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1000 * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1499 milliseconds with target unit second should be 1 second")
} {
SECTION("round 1001 milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1499 * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1001 * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1500 milliseconds with target unit second should be 2 seconds")
} {
SECTION("round 1499 milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1500 * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1499 * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1999 milliseconds with target unit second should be 2 seconds")
} {
SECTION("round 1500 milliseconds with target unit second should be 2 seconds") REQUIRE(round<si::second>(1999 * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1500 * isq::time[ms]) == 2 * isq::time[s]); SECTION("round -1000 milliseconds with target unit second should be -1 second")
} {
SECTION("round 1999 milliseconds with target unit second should be 2 seconds") REQUIRE(round<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1999 * isq::time[ms]) == 2 * isq::time[s]); SECTION("round -1001 milliseconds with target unit second should be -1 second")
} {
SECTION("round -1000 milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1001 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1000 * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1499 milliseconds with target unit second should be -1 second")
} {
SECTION("round -1001 milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1499 * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1001 * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1500 milliseconds with target unit second should be -2 seconds")
} {
SECTION("round -1499 milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1500 * isq::time[ms]) == -2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1499 * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1999 milliseconds with target unit second should be -2 seconds")
} {
SECTION("round -1500 milliseconds with target unit second should be -2 seconds") REQUIRE(round<si::second>(-1999 * isq::time[ms]) == -2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1500 * isq::time[ms]) == -2 * isq::time[s]); SECTION("round 1000. milliseconds with target unit second should be 1 second")
} {
SECTION("round -1999 milliseconds with target unit second should be -2 seconds") REQUIRE(round<si::second>(1000. * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1999 * isq::time[ms]) == -2 * isq::time[s]); SECTION("round 1001. milliseconds with target unit second should be 1 second")
} {
SECTION("round 1000. milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1001. * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1000. * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1499. milliseconds with target unit second should be 1 second")
} {
SECTION("round 1001. milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1499. * isq::time[ms]) == 1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1001. * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1500. milliseconds with target unit second should be 2 seconds")
} {
SECTION("round 1499. milliseconds with target unit second should be 1 second") REQUIRE(round<si::second>(1500. * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1499. * isq::time[ms]) == 1 * isq::time[s]); SECTION("round 1999. milliseconds with target unit second should be 2 seconds")
} {
SECTION("round 1500. milliseconds with target unit second should be 2 seconds") REQUIRE(round<si::second>(1999. * isq::time[ms]) == 2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1500. * isq::time[ms]) == 2 * isq::time[s]); SECTION("round -1000. milliseconds with target unit second should be -1 second")
} {
SECTION("round 1999. milliseconds with target unit second should be 2 seconds") REQUIRE(round<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(1999. * isq::time[ms]) == 2 * isq::time[s]); SECTION("round -1001. milliseconds with target unit second should be -1 second")
} {
SECTION("round -1000. milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1001. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1000. * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1499. milliseconds with target unit second should be -1 second")
} {
SECTION("round -1001. milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1499. * isq::time[ms]) == -1 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1001. * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1500. milliseconds with target unit second should be -2 seconds")
} {
SECTION("round -1499. milliseconds with target unit second should be -1 second") REQUIRE(round<si::second>(-1500. * isq::time[ms]) == -2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1499. * isq::time[ms]) == -1 * isq::time[s]); SECTION("round -1999. milliseconds with target unit second should be -2 seconds")
} {
SECTION("round -1500. milliseconds with target unit second should be -2 seconds") REQUIRE(round<si::second>(-1999. * isq::time[ms]) == -2 * isq::time[s]);
{ }
REQUIRE(round<si::second>(-1500. * isq::time[ms]) == -2 * isq::time[s]); }
}
SECTION("round -1999. milliseconds with target unit second should be -2 seconds") SECTION("hypot functions")
{ {
REQUIRE(round<si::second>(-1999. * isq::time[ms]) == -2 * isq::time[s]); SECTION("hypot should work on the same quantities")
} {
} REQUIRE(hypot(3. * isq::length[km], 4. * isq::length[km]) == 5. * isq::length[km]);
REQUIRE(hypot(2. * isq::length[km], 3. * isq::length[km], 6. * isq::length[km]) == 7. * isq::length[km]);
TEST_CASE("hypot functions", "[hypot]") }
{ SECTION("hypot should work with different units of the same dimension")
SECTION("hypot should work on the same quantities") {
{ REQUIRE(hypot(3. * isq::length[km], 4000. * isq::length[m]) == 5. * isq::length[km]);
REQUIRE(hypot(3. * isq::length[km], 4. * isq::length[km]) == 5. * isq::length[km]); REQUIRE(hypot(2. * isq::length[km], 3000. * isq::length[m], 6. * isq::length[km]) == 7. * isq::length[km]);
REQUIRE(hypot(2. * isq::length[km], 3. * isq::length[km], 6. * isq::length[km]) == 7. * isq::length[km]); }
} }
SECTION("hypot should work with different units of the same dimension")
{ SECTION("SI trigonometric functions")
REQUIRE(hypot(3. * isq::length[km], 4000. * isq::length[m]) == 5. * isq::length[km]); {
REQUIRE(hypot(2. * isq::length[km], 3000. * isq::length[m], 6. * isq::length[km]) == 7. * isq::length[km]); SECTION("sin")
} {
} REQUIRE_THAT(si::sin(0 * deg), AlmostEquals(0. * one));
REQUIRE_THAT(si::sin(90 * deg), AlmostEquals(1. * one));
TEST_CASE("SI trigonometric functions", "[trig][si]") REQUIRE_THAT(si::sin(180 * deg), AlmostEquals(0. * one));
{ REQUIRE_THAT(si::sin(270 * deg), AlmostEquals(-1. * one));
SECTION("sin") }
{
REQUIRE_THAT(si::sin(0 * deg), AlmostEquals(0. * one)); SECTION("cos")
REQUIRE_THAT(si::sin(90 * deg), AlmostEquals(1. * one)); {
REQUIRE_THAT(si::sin(180 * deg), AlmostEquals(0. * one)); REQUIRE_THAT(si::cos(0 * deg), AlmostEquals(1. * one));
REQUIRE_THAT(si::sin(270 * deg), AlmostEquals(-1. * one)); REQUIRE_THAT(si::cos(90 * deg), AlmostEquals(0. * one));
} REQUIRE_THAT(si::cos(180 * deg), AlmostEquals(-1. * one));
REQUIRE_THAT(si::cos(270 * deg), AlmostEquals(0. * one));
SECTION("cos") }
{
REQUIRE_THAT(si::cos(0 * deg), AlmostEquals(1. * one)); SECTION("tan")
REQUIRE_THAT(si::cos(90 * deg), AlmostEquals(0. * one)); {
REQUIRE_THAT(si::cos(180 * deg), AlmostEquals(-1. * one)); REQUIRE_THAT(si::tan(0 * deg), AlmostEquals(0. * one));
REQUIRE_THAT(si::cos(270 * deg), AlmostEquals(0. * one)); REQUIRE_THAT(si::tan(45. * deg), AlmostEquals(1. * one));
} REQUIRE_THAT(si::tan(135. * deg), AlmostEquals(-1. * one));
REQUIRE_THAT(si::tan(180. * deg), AlmostEquals(0. * one));
SECTION("tan") }
{ }
REQUIRE_THAT(si::tan(0 * deg), AlmostEquals(0. * one));
REQUIRE_THAT(si::tan(45. * deg), AlmostEquals(1. * one)); SECTION("SI inverse trigonometric functions")
REQUIRE_THAT(si::tan(135. * deg), AlmostEquals(-1. * one)); {
REQUIRE_THAT(si::tan(180. * deg), AlmostEquals(0. * one)); SECTION("asin")
} {
} REQUIRE_THAT(si::asin(-1 * one), AlmostEquals(-90. * deg));
REQUIRE_THAT(si::asin(0 * one), AlmostEquals(0. * deg));
TEST_CASE("SI inverse trigonometric functions", "[inv trig][si]") REQUIRE_THAT(si::asin(1 * one), AlmostEquals(90. * deg));
{ }
SECTION("asin")
{ SECTION("acos")
REQUIRE_THAT(si::asin(-1 * one), AlmostEquals(-90. * deg)); {
REQUIRE_THAT(si::asin(0 * one), AlmostEquals(0. * deg)); REQUIRE_THAT(si::asin(-1 * one), AlmostEquals(-90. * deg));
REQUIRE_THAT(si::asin(1 * one), AlmostEquals(90. * deg)); REQUIRE_THAT(si::asin(0 * one), AlmostEquals(0. * deg));
} REQUIRE_THAT(si::asin(1 * one), AlmostEquals(90. * deg));
}
SECTION("acos")
{ SECTION("atan")
REQUIRE_THAT(si::asin(-1 * one), AlmostEquals(-90. * deg)); {
REQUIRE_THAT(si::asin(0 * one), AlmostEquals(0. * deg)); REQUIRE_THAT(si::atan(-1 * one), AlmostEquals(-45. * deg));
REQUIRE_THAT(si::asin(1 * one), AlmostEquals(90. * deg)); REQUIRE_THAT(si::atan(0 * one), AlmostEquals(0. * deg));
} REQUIRE_THAT(si::atan(1 * one), AlmostEquals(45. * deg));
}
SECTION("atan") }
{
REQUIRE_THAT(si::atan(-1 * one), AlmostEquals(-45. * deg)); SECTION("SI atan2 functions")
REQUIRE_THAT(si::atan(0 * one), AlmostEquals(0. * deg)); {
REQUIRE_THAT(si::atan(1 * one), AlmostEquals(45. * deg)); SECTION("atan2 should work on the same quantities")
} {
} REQUIRE_THAT(si::atan2(-1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(-45. * deg));
REQUIRE_THAT(si::atan2(0. * isq::length[km], 1. * isq::length[km]), AlmostEquals(0. * deg));
TEST_CASE("SI atan2 functions", "[atan2][si]") REQUIRE_THAT(si::atan2(1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(45. * deg));
{ }
SECTION("atan2 should work on the same quantities") SECTION("atan2 should work with different units of the same dimension")
{ {
REQUIRE_THAT(si::atan2(-1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(-45. * deg)); REQUIRE_THAT(si::atan2(-1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(-45. * deg));
REQUIRE_THAT(si::atan2(0. * isq::length[km], 1. * isq::length[km]), AlmostEquals(0. * deg)); REQUIRE_THAT(si::atan2(0. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(0. * deg));
REQUIRE_THAT(si::atan2(1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(45. * deg)); REQUIRE_THAT(si::atan2(1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(45. * deg));
} }
SECTION("atan2 should work with different units of the same dimension") }
{
REQUIRE_THAT(si::atan2(-1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(-45. * deg)); SECTION("Angle trigonometric functions")
REQUIRE_THAT(si::atan2(0. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(0. * deg)); {
REQUIRE_THAT(si::atan2(1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(45. * deg)); using namespace mp_units::angular;
} using namespace mp_units::angular::unit_symbols;
} using mp_units::angular::unit_symbols::deg;
SECTION("sin")
TEST_CASE("Angle trigonometric functions", "[trig][angle]") {
{ REQUIRE_THAT(sin(0 * angle[deg]), AlmostEquals(0. * one));
using namespace mp_units::angular; REQUIRE_THAT(sin(90 * angle[deg]), AlmostEquals(1. * one));
using namespace mp_units::angular::unit_symbols; REQUIRE_THAT(sin(180 * angle[deg]), AlmostEquals(0. * one));
using mp_units::angular::unit_symbols::deg; REQUIRE_THAT(sin(270 * angle[deg]), AlmostEquals(-1. * one));
SECTION("sin") REQUIRE_THAT(sin(0 * angle[grad]), AlmostEquals(0. * one));
{ REQUIRE_THAT(sin(100 * angle[grad]), AlmostEquals(1. * one));
REQUIRE_THAT(sin(0 * angle[deg]), AlmostEquals(0. * one)); REQUIRE_THAT(sin(200 * angle[grad]), AlmostEquals(0. * one, 2));
REQUIRE_THAT(sin(90 * angle[deg]), AlmostEquals(1. * one)); REQUIRE_THAT(sin(300 * angle[grad]), AlmostEquals(-1. * one));
REQUIRE_THAT(sin(180 * angle[deg]), AlmostEquals(0. * one)); }
REQUIRE_THAT(sin(270 * angle[deg]), AlmostEquals(-1. * one));
SECTION("cos")
REQUIRE_THAT(sin(0 * angle[grad]), AlmostEquals(0. * one)); {
REQUIRE_THAT(sin(100 * angle[grad]), AlmostEquals(1. * one)); REQUIRE_THAT(cos(0 * angle[deg]), AlmostEquals(1. * one));
REQUIRE_THAT(sin(200 * angle[grad]), AlmostEquals(0. * one, 2)); REQUIRE_THAT(cos(90 * angle[deg]), AlmostEquals(0. * one));
REQUIRE_THAT(sin(300 * angle[grad]), AlmostEquals(-1. * one)); REQUIRE_THAT(cos(180 * angle[deg]), AlmostEquals(-1. * one));
} REQUIRE_THAT(cos(270 * angle[deg]), AlmostEquals(0. * one));
SECTION("cos") REQUIRE_THAT(cos(0 * angle[grad]), AlmostEquals(1. * one));
{ REQUIRE_THAT(cos(100 * angle[grad]), AlmostEquals(0. * one));
REQUIRE_THAT(cos(0 * angle[deg]), AlmostEquals(1. * one)); REQUIRE_THAT(cos(200 * angle[grad]), AlmostEquals(-1. * one));
REQUIRE_THAT(cos(90 * angle[deg]), AlmostEquals(0. * one)); REQUIRE_THAT(cos(300 * angle[grad]), AlmostEquals(0. * one));
REQUIRE_THAT(cos(180 * angle[deg]), AlmostEquals(-1. * one)); }
REQUIRE_THAT(cos(270 * angle[deg]), AlmostEquals(0. * one));
SECTION("tan")
REQUIRE_THAT(cos(0 * angle[grad]), AlmostEquals(1. * one)); {
REQUIRE_THAT(cos(100 * angle[grad]), AlmostEquals(0. * one)); REQUIRE_THAT(tan(0 * angle[deg]), AlmostEquals(0. * one));
REQUIRE_THAT(cos(200 * angle[grad]), AlmostEquals(-1. * one)); REQUIRE_THAT(tan(45 * angle[deg]), AlmostEquals(1. * one));
REQUIRE_THAT(cos(300 * angle[grad]), AlmostEquals(0. * one)); REQUIRE_THAT(tan(135 * angle[deg]), AlmostEquals(-1. * one));
} REQUIRE_THAT(tan(180 * angle[deg]), AlmostEquals(0. * one));
SECTION("tan") REQUIRE_THAT(tan(0 * angle[grad]), AlmostEquals(0. * one));
{ REQUIRE_THAT(tan(50 * angle[grad]), AlmostEquals(1. * one));
REQUIRE_THAT(tan(0 * angle[deg]), AlmostEquals(0. * one)); REQUIRE_THAT(tan(150 * angle[grad]), AlmostEquals(-1. * one));
REQUIRE_THAT(tan(45 * angle[deg]), AlmostEquals(1. * one)); REQUIRE_THAT(tan(200 * angle[grad]), AlmostEquals(0. * one, 2));
REQUIRE_THAT(tan(135 * angle[deg]), AlmostEquals(-1. * one)); }
REQUIRE_THAT(tan(180 * angle[deg]), AlmostEquals(0. * one)); }
REQUIRE_THAT(tan(0 * angle[grad]), AlmostEquals(0. * one)); SECTION("Angle inverse trigonometric functions")
REQUIRE_THAT(tan(50 * angle[grad]), AlmostEquals(1. * one)); {
REQUIRE_THAT(tan(150 * angle[grad]), AlmostEquals(-1. * one)); using namespace mp_units::angular;
REQUIRE_THAT(tan(200 * angle[grad]), AlmostEquals(0. * one, 2)); using namespace mp_units::angular::unit_symbols;
} using mp_units::angular::unit_symbols::deg;
}
SECTION("asin")
TEST_CASE("Angle inverse trigonometric functions", "[inv trig][angle]") {
{ REQUIRE_THAT(asin(-1 * one), AlmostEquals(-90. * angle[deg]));
using namespace mp_units::angular; REQUIRE_THAT(asin(0 * one), AlmostEquals(0. * angle[deg]));
using namespace mp_units::angular::unit_symbols; REQUIRE_THAT(asin(1 * one), AlmostEquals(90. * angle[deg]));
using mp_units::angular::unit_symbols::deg; }
SECTION("asin") SECTION("acos")
{ {
REQUIRE_THAT(asin(-1 * one), AlmostEquals(-90. * angle[deg])); REQUIRE_THAT(asin(-1 * one), AlmostEquals(-90. * angle[deg]));
REQUIRE_THAT(asin(0 * one), AlmostEquals(0. * angle[deg])); REQUIRE_THAT(asin(0 * one), AlmostEquals(0. * angle[deg]));
REQUIRE_THAT(asin(1 * one), AlmostEquals(90. * angle[deg])); REQUIRE_THAT(asin(1 * one), AlmostEquals(90. * angle[deg]));
} }
SECTION("acos") SECTION("atan")
{ {
REQUIRE_THAT(asin(-1 * one), AlmostEquals(-90. * angle[deg])); REQUIRE_THAT(atan(-1 * one), AlmostEquals(-45. * angle[deg]));
REQUIRE_THAT(asin(0 * one), AlmostEquals(0. * angle[deg])); REQUIRE_THAT(atan(0 * one), AlmostEquals(0. * angle[deg]));
REQUIRE_THAT(asin(1 * one), AlmostEquals(90. * angle[deg])); REQUIRE_THAT(atan(1 * one), AlmostEquals(45. * angle[deg]));
} }
}
SECTION("atan")
{ SECTION("Angle atan2 functions")
REQUIRE_THAT(atan(-1 * one), AlmostEquals(-45. * angle[deg])); {
REQUIRE_THAT(atan(0 * one), AlmostEquals(0. * angle[deg])); using namespace mp_units::angular;
REQUIRE_THAT(atan(1 * one), AlmostEquals(45. * angle[deg])); using namespace mp_units::angular::unit_symbols;
} using mp_units::angular::unit_symbols::deg;
}
SECTION("atan2 should work on the same quantities")
TEST_CASE("Angle atan2 functions", "[atan2][angle]") {
{ REQUIRE_THAT(atan2(-1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(-45. * angle[deg]));
using namespace mp_units::angular; REQUIRE_THAT(atan2(0. * isq::length[km], 1. * isq::length[km]), AlmostEquals(0. * angle[deg]));
using namespace mp_units::angular::unit_symbols; REQUIRE_THAT(atan2(1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(45. * angle[deg]));
using mp_units::angular::unit_symbols::deg; }
SECTION("atan2 should work with different units of the same dimension")
SECTION("atan2 should work on the same quantities") {
{ REQUIRE_THAT(atan2(-1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(-45. * angle[deg]));
REQUIRE_THAT(atan2(-1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(-45. * angle[deg])); REQUIRE_THAT(atan2(0. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(0. * angle[deg]));
REQUIRE_THAT(atan2(0. * isq::length[km], 1. * isq::length[km]), AlmostEquals(0. * angle[deg])); REQUIRE_THAT(atan2(1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(45. * angle[deg]));
REQUIRE_THAT(atan2(1. * isq::length[km], 1. * isq::length[km]), AlmostEquals(45. * angle[deg])); }
}
SECTION("atan2 should work with different units of the same dimension")
{
REQUIRE_THAT(atan2(-1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(-45. * angle[deg]));
REQUIRE_THAT(atan2(0. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(0. * angle[deg]));
REQUIRE_THAT(atan2(1. * isq::length[km], 1000. * isq::length[m]), AlmostEquals(45. * angle[deg]));
} }
} }

View File

@@ -60,22 +60,25 @@ constexpr bool within_4_ulps(T a, T b)
} // namespace } // namespace
// conversion requiring radical magnitudes TEST_CASE("quantity operations", "[quantity]")
TEST_CASE("unit conversions support radical magnitudes", "[conversion][radical]")
{ {
REQUIRE(within_4_ulps(sqrt((1.0 * m) * (1.0 * km)).numerical_value_in(m), sqrt(1000.0))); // conversion requiring radical magnitudes
} SECTION("unit conversions support radical magnitudes")
{
REQUIRE(within_4_ulps(sqrt((1.0 * m) * (1.0 * km)).numerical_value_in(m), sqrt(1000.0)));
}
// Reproducing issue #474 exactly: // Reproducing issue #474 exactly:
TEST_CASE("Issue 474 is fixed", "[conversion][radical]") SECTION("Issue 474 is fixed")
{ {
constexpr auto val_issue_474 = 8.0 * si::si2019::boltzmann_constant * 1000.0 * K / (std::numbers::pi * 10 * Da); constexpr auto val_issue_474 = 8.0 * si::si2019::boltzmann_constant * 1000.0 * K / (std::numbers::pi * 10 * Da);
REQUIRE(within_4_ulps(sqrt(val_issue_474).numerical_value_in(m / s), REQUIRE(within_4_ulps(sqrt(val_issue_474).numerical_value_in(m / s),
sqrt(val_issue_474.numerical_value_in(m * m / s / s)))); sqrt(val_issue_474.numerical_value_in(m * m / s / s))));
} }
TEST_CASE("Volatile representation type", "[volatile]") SECTION("Volatile representation type")
{ {
volatile std::int16_t vint = 123; volatile std::int16_t vint = 123;
REQUIRE(quantity(vint * m).numerical_value_in(m) == 123); REQUIRE(quantity(vint * m).numerical_value_in(m) == 123);
}
} }

View File

@@ -25,24 +25,28 @@
#include <mp-units/systems/isq/mechanics.h> #include <mp-units/systems/isq/mechanics.h>
#include <mp-units/systems/isq/space_and_time.h> #include <mp-units/systems/isq/space_and_time.h>
#include <mp-units/systems/si/units.h> #include <mp-units/systems/si/units.h>
#if MP_UNITS_HOSTED
template<class T> #include <mp-units/cartesian_vector.h>
requires mp_units::is_scalar<T> #endif
constexpr bool mp_units::is_vector<T> = true;
namespace { namespace {
using namespace mp_units; using namespace mp_units;
using namespace mp_units::cgs; using namespace mp_units::cgs;
using namespace mp_units::cgs::unit_symbols; using namespace mp_units::cgs::unit_symbols;
#if MP_UNITS_HOSTED
using v = cartesian_vector<double>;
#endif
// https://en.wikipedia.org/wiki/Centimetre%E2%80%93gram%E2%80%93second_system_of_units#Definitions_and_conversion_factors_of_CGS_units_in_mechanics // https://en.wikipedia.org/wiki/Centimetre%E2%80%93gram%E2%80%93second_system_of_units#Definitions_and_conversion_factors_of_CGS_units_in_mechanics
static_assert(isq::length(100 * cm) == isq::length(1 * si::metre)); static_assert(isq::length(100 * cm) == isq::length(1 * si::metre));
static_assert(isq::mass(1000 * g) == isq::mass(1 * si::kilogram)); static_assert(isq::mass(1000 * g) == isq::mass(1 * si::kilogram));
static_assert(isq::time(1 * s) == isq::time(1 * si::second)); static_assert(isq::time(1 * s) == isq::time(1 * si::second));
static_assert(isq::speed(100 * cm / s) == isq::speed(1 * si::metre / si::second)); static_assert(isq::speed(100 * cm / s) == isq::speed(1 * si::metre / si::second));
static_assert(isq::acceleration(100 * Gal) == isq::acceleration(1 * si::metre / square(si::second))); #if MP_UNITS_HOSTED
static_assert(isq::force(100'000 * dyn) == isq::force(1 * si::newton)); static_assert(isq::acceleration(v{100} * Gal) == isq::acceleration(v{1} * si::metre / square(si::second)));
static_assert(isq::force(v{100'000} * dyn) == isq::force(v{1} * si::newton));
#endif
static_assert(isq::energy(10'000'000 * erg) == isq::energy(1 * si::joule)); static_assert(isq::energy(10'000'000 * erg) == isq::energy(1 * si::joule));
static_assert(isq::power(10'000'000 * erg / s) == isq::power(1 * si::watt)); static_assert(isq::power(10'000'000 * erg / s) == isq::power(1 * si::watt));
static_assert(isq::pressure(10 * Ba) == isq::pressure(1 * si::pascal)); static_assert(isq::pressure(10 * Ba) == isq::pressure(1 * si::pascal));

View File

@@ -237,17 +237,17 @@ static_assert(compare(round<si::second>(-1999. * isq::time[ms]), -2. * isq::time
#endif #endif
// non-truncating // non-truncating
static_assert(compare(inverse<us>(250 * Hz), 4000 * us)); static_assert(compare(kind_of<isq::time>(inverse<us>(250 * Hz)), 4000 * us));
static_assert(compare(inverse<us>(250 * kHz), 4 * us)); static_assert(compare(kind_of<isq::time>(inverse<us>(250 * kHz)), 4 * us));
static_assert(compare(inverse<ks>(250 * uHz), 4 * ks)); static_assert(compare(kind_of<isq::time>(inverse<ks>(250 * uHz)), 4 * ks));
// truncating // truncating
static_assert(compare(inverse<s>(1 * kHz), 0 * s)); static_assert(compare(kind_of<isq::time>(inverse<s>(1 * kHz)), 0 * s));
// floating-point representation does not truncate // floating-point representation does not truncate
static_assert(compare(inverse<s>(1. * kHz), 0.001 * s)); static_assert(compare(kind_of<isq::time>(inverse<s>(1. * kHz)), 0.001 * s));
// check if constraints work properly for a derived unit of a narrowed kind // check if constraints work properly for a derived unit of a narrowed kind
static_assert(compare(inverse<Hz>(1 * s), 1 * Hz)); static_assert(compare(kind_of<isq::frequency>(inverse<Hz>(1 * s)), 1 * Hz));
} // namespace } // namespace

View File

@@ -378,4 +378,14 @@ static_assert(invalid_comparison<frequency(1 * hertz), activity(1 * becquerel)>)
static_assert(invalid_comparison<angular_measure(1 * radian), solid_angular_measure(1 * steradian)>); static_assert(invalid_comparison<angular_measure(1 * radian), solid_angular_measure(1 * steradian)>);
static_assert(invalid_comparison<angular_measure(1 * radian), storage_capacity(1 * bit)>); static_assert(invalid_comparison<angular_measure(1 * radian), storage_capacity(1 * bit)>);
// make_reference
static_assert(is_of_type<make_reference(length, metre), reference<length_, metre_>>);
static_assert(is_of_type<make_reference(width, metre), reference<width_, metre_>>);
static_assert(is_of_type<make_reference(kind_of<length>, metre), metre_>);
static_assert(is_of_type<make_reference(get_quantity_spec(metre), metre), metre_>);
static_assert(is_of_type<make_reference(get_quantity_spec(hertz), hertz), hertz_>);
static_assert(is_of_type<make_reference(kind_of<frequency>, hertz), hertz_>);
static_assert(is_of_type<make_reference(get_quantity_spec(watt), watt), watt_>);
static_assert(is_of_type<make_reference(kind_of<power>, watt), reference<kind_of_<power_>, watt_>>);
} // namespace } // namespace