From 3f17f2f01f9ff2e4d353db16399a238ef58ebf46 Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Thu, 12 Feb 2026 09:11:11 +0100 Subject: [PATCH 1/3] Base: Fix schema translation data Corrects 12 bugs across Internal and MKS schemas where factors were wrong, units were assigned to the wrong threshold positions, or thresholds didn't align with natural unit boundaries. Co-Authored-By: Claude Opus 4.6 --- src/Base/UnitsSchemasData.h | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Base/UnitsSchemasData.h b/src/Base/UnitsSchemasData.h index 0d4c8ee00b..99a7fdf13c 100644 --- a/src/Base/UnitsSchemasData.h +++ b/src/Base/UnitsSchemasData.h @@ -96,7 +96,7 @@ inline const UnitsSchemaSpec s3 { 1e4 , "mm" , 1.0 }, { 1e7 , "m" , 1e3 }, { 1e10 , "km" , 1e6 }, - { 0 , "m" , 1.0 }} + { 0 , "m" , 1e3 }} }, { "Area", { { 1e2 , "mm^2" , 1.0 }, @@ -114,7 +114,7 @@ inline const UnitsSchemaSpec s3 { 0 , "°" , 1.0 }} }, { "Mass", { - { 1e-6 , "\xC2\xB5g" , 1.0 }, + { 1e-6 , "\xC2\xB5g" , 1e-9 }, { 1e-3 , "mg" , 1e-6 }, { 1.0 , "g" , 1e-3 }, { 1e3 , "kg" , 1.0 }, @@ -126,8 +126,8 @@ inline const UnitsSchemaSpec s3 { 0 , "kg/mm^3" , 1.0 }} }, { "ThermalConductivity", { - { 0 , "W/m/K" , 1e3 }, - { 1e6 , "W/mm/K" , 1e6 }} + { 1e6 , "W/m/K" , 1e3 }, + { 0 , "W/mm/K" , 1e6 }} }, { "ThermalExpansionCoefficient", { { 1e-3 , "\xC2\xB5m/m/K" , 1e-6 }, @@ -209,18 +209,18 @@ inline const UnitsSchemaSpec s3 { 0 , "C" , 1.0 }} }, { "SurfaceChargeDensity", { - { 1e-4 , "C/m^2" , 1e-6 }, - { 1e-2 , "C/cm^2" , 1e-2 }, + { 1e-2 , "C/m^2" , 1e-6 }, + { 1.0 , "C/cm^2" , 1e-2 }, { 0 , "C/mm^2" , 1.0 }} }, { "VolumeChargeDensity", { - { 1e-4 , "C/m^3" , 1e-9 }, - { 1e-2 , "C/cm^3" , 1e-3 }, + { 1e-3 , "C/m^3" , 1e-9 }, + { 1.0 , "C/cm^3" , 1e-3 }, { 0 , "C/mm^3" , 1.0 }} }, { "CurrentDensity", { - { 1e-4 , "A/m^2" , 1e-6 }, - { 1e-2 , "A/cm^2" , 1e-2 }, + { 1e-2 , "A/m^2" , 1e-6 }, + { 1.0 , "A/cm^2" , 1e-2 }, { 0 , "A/mm^2" , 1 }} }, { "MagneticFluxDensity", { @@ -250,10 +250,10 @@ inline const UnitsSchemaSpec s3 { 0 , "MOhm" , 1e12 }} }, { "ElectricalConductivity", { - { 0 , "MS/m" , 1e-3 }, - { 1e-3 , "mS/m" , 1e-12 }, - { 1.0 , "S/m" , 1e-9 }, - { 1e3 , "kS/m" , 1e-6 }} + { 1e-9 , "mS/m" , 1e-12 }, + { 1e-6 , "S/m" , 1e-9 }, + { 1e-3 , "kS/m" , 1e-6 }, + { 0 , "MS/m" , 1e-3 }} }, { "ElectricalCapacitance", { { 1e-15 , "pF" , 1e-18 }, @@ -366,7 +366,7 @@ inline const UnitsSchemaSpec s4 { 10'000.0 , "kPa" , 1.0 }, { 10'000'000.0 , "MPa" , 1'000.0 }, { 10'000'000'000.0 , "GPa" , 1'000'000.0 }, - { 0 , "Pa" , 1000.0 }} + { 0 , "Pa" , 0.001 }} }, { "Stress", { { 10.0 , "Pa" , 0.001 }, @@ -388,8 +388,8 @@ inline const UnitsSchemaSpec s4 { 0 , "GPa/m" , 1e3 }} }, { "ThermalConductivity", { - { 1'000'000 , "W/mm/K" , 1'000'000.0 }, - { 0 , "W/m/K" , 1'000.0 }} + { 1'000'000 , "W/m/K" , 1'000.0 }, + { 0 , "W/mm/K" , 1'000'000.0 }} }, { "ThermalExpansionCoefficient", { { 0.001 , "\xC2\xB5m/m/K" , 0.000001 }, @@ -432,7 +432,7 @@ inline const UnitsSchemaSpec s4 { 0 , "C/m^3" , 1e-9 }} }, { "CurrentDensity", { - { 1e3 , "A/m^2" , 1e-6 }, + { 1.0 , "A/m^2" , 1e-6 }, { 0 , "A/mm^2" , 1.0 }} }, { "MagneticFluxDensity", { @@ -462,9 +462,9 @@ inline const UnitsSchemaSpec s4 { 0 , "MOhm" , 1e12 }} }, { "ElectricalConductivity", { - { 1e-3 , "mS/m" , 1e-12 }, - { 1.0 , "S/m" , 1e-9 }, - { 1e3 , "kS/m" , 1e-6 }, + { 1e-9 , "mS/m" , 1e-12 }, + { 1e-6 , "S/m" , 1e-9 }, + { 1e-3 , "kS/m" , 1e-6 }, { 0 , "MS/m" , 1e-3 }} }, { "ElectricalCapacitance", { @@ -475,9 +475,9 @@ inline const UnitsSchemaSpec s4 { 0 , "F" , 1e-6 }} }, { "ElectricalInductance", { - { 1e-6 , "nH" , 1e-3 }, - { 1e-3 , "\xC2\xB5H" , 1.0 }, - { 1.0 , "mH" , 1e3 }, + { 1.0 , "nH" , 1e-3 }, + { 1e3 , "\xC2\xB5H" , 1.0 }, + { 1e6 , "mH" , 1e3 }, { 0 , "H" , 1e6 }} }, { "VacuumPermittivity", { From bb977afe552d0bbe7206cdcb8d14e59a8c51b94e Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Thu, 12 Feb 2026 12:27:32 +0100 Subject: [PATCH 2/3] Base: Fix floating point issue in schema threshold comparison When a parsed value lands exactly at a threshold boundary (e.g. "1 S/m" = 1e-9 at threshold 1e-9), floating point rounding could cause the wrong unit to be selected. Shrink thresholds by a relative epsilon so boundary values fall through to the next (more natural) unit. Co-Authored-By: Claude Opus 4.6 --- src/Base/UnitsSchema.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Base/UnitsSchema.cpp b/src/Base/UnitsSchema.cpp index decf368f9f..eb577907d5 100644 --- a/src/Base/UnitsSchema.cpp +++ b/src/Base/UnitsSchema.cpp @@ -67,7 +67,11 @@ std::string UnitsSchema::translate(const Quantity& quant, double& factor, std::s const auto value = quant.getValue(); auto isSuitable = [&](const UnitTranslationSpec& row) { - return row.threshold > value || row.threshold == 0; // zero indicates default + // Shrink threshold slightly so values at exact threshold boundaries + // (e.g. "1 S/m" = 1e-9 at threshold 1e-9) fall through to the next unit. + constexpr double relEps = 1e-12; + return row.threshold * (1.0 - relEps) > value + || row.threshold == 0; // zero indicates default }; auto unitSpecs = spec.translationSpecs.at(unitName); From d94269f85f8833a2860b323ab5ff3b828c354e2a Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Thu, 12 Feb 2026 12:28:36 +0100 Subject: [PATCH 3/3] Base: Add sweep round-trip tests for all unit schemas Sweep tests parse a string and translate it back, verifying identical output. Values cover every threshold band at 10x increments plus a scientific-notation value in each default band. Tests all 10 schemas. Co-Authored-By: Claude Opus 4.6 --- tests/src/Base/SchemaTests.cpp | 579 +++++++++++++++++++++++++++++++++ 1 file changed, 579 insertions(+) diff --git a/tests/src/Base/SchemaTests.cpp b/tests/src/Base/SchemaTests.cpp index 2e2f87cf91..4d0b295c6d 100644 --- a/tests/src/Base/SchemaTests.cpp +++ b/tests/src/Base/SchemaTests.cpp @@ -90,6 +90,22 @@ protected: return quantity.getSafeUserString(); } + static void sweepCheck(std::initializer_list> groups) + { + for (const auto& group : groups) { + for (const char* str : group) { + SCOPED_TRACE(str); + auto q = Quantity::parse(str); + QuantityFormat fmt(QuantityFormat::Default); + q.setFormat(fmt); + double factor {}; + std::string unitString; + auto result = UnitsApi::schemaTranslate(q, factor, unitString); + EXPECT_EQ(result, str); + } + } + } + std::unique_ptr schemas; // NOLINT }; @@ -690,3 +706,566 @@ TEST_F(SchemaTest, round_trip_test) } } } + +// Sweep round-trip tests: parse a string, translate it back, verify identical output. +// Each string is both the input and the expected result. Values are chosen to land +// cleanly in each threshold band so the unit selection is tested across the full range. + +TEST_F(SchemaTest, sweep_internal) +{ + UnitsApi::setSchema("Internal"); + UnitsApi::setDecimals(6); + sweepCheck({ + // Length + {"1 nm", + "10 nm", + "100 nm", + "1 \xC2\xB5m", + "10 \xC2\xB5m", + "1 mm", + "10 mm", + "100 mm", + "1000 mm", + "10 m", + "100 m", + "1000 m", + "10 km", + "100 km", + "1000 km", + /* default */ "1e+09 m"}, + // Mass + {"1 \xC2\xB5g", + "10 \xC2\xB5g", + "100 \xC2\xB5g", + "1 mg", + "10 mg", + "100 mg", + "1 g", + "10 g", + "100 g", + "1 kg", + "10 kg", + "100 kg", + "1 t", + "10 t", + /* default */ "1e+06 t"}, + // Area + {"1 mm^2", + "10 mm^2", + "1 cm^2", + "10 cm^2", + "100 cm^2", + "1000 cm^2", + "1 m^2", + "10 m^2", + "100 m^2", + "1000 m^2", + "1 km^2", + /* default */ "1e+06 km^2"}, + // Volume + {"1 mm^3", + "10 mm^3", + "100 mm^3", + "1 ml", + "10 ml", + "100 ml", + "1 l", + "10 l", + "100 l", + "1 m^3", + "10 m^3", + /* default */ "1e+06 m^3"}, + // Pressure + {"1 Pa", + "10 Pa", + "100 Pa", + "1000 Pa", + "10 kPa", + "100 kPa", + "1000 kPa", + "10 MPa", + "100 MPa", + "1000 MPa", + "10 GPa", + "100 GPa", + "1000 GPa", + /* default */ "1e+15 Pa"}, + // Force + {"1 mN", + "10 mN", + "100 mN", + "1 N", + "10 N", + "100 N", + "1 kN", + "10 kN", + "100 kN", + "1 MN", + "10 MN", + /* default */ "1e+06 MN"}, + // Power + {"1 mW", + "10 mW", + "100 mW", + "1 W", + "10 W", + "100 W", + "1 kW", + "10 kW", + /* default */ "1e+06 kW"}, + // ElectricPotential + {"1 mV", + "10 mV", + "100 mV", + "1 V", + "10 V", + "100 V", + "1 kV", + "10 kV", + "100 kV", + /* default */ "1e+07 V"}, + // Frequency + {"1 Hz", + "10 Hz", + "100 Hz", + "1 kHz", + "10 kHz", + "100 kHz", + "1 MHz", + "10 MHz", + "100 MHz", + "1 GHz", + "10 GHz", + "100 GHz", + "1 THz", + /* default */ "1e+06 THz"}, + // ThermalConductivity + {"1 W/m/K", + "10 W/m/K", + "100 W/m/K", + "1 W/mm/K", + "10 W/mm/K", + /* default */ "1e+06 W/mm/K"}, + // ElectricalConductivity + {"1 mS/m", + "10 mS/m", + "100 mS/m", + "1 S/m", + "10 S/m", + "100 S/m", + "1 kS/m", + "10 kS/m", + "100 kS/m", + "1 MS/m", + /* default */ "1e+06 MS/m"}, + // SurfaceChargeDensity + {"1 C/m^2", + "10 C/m^2", + "100 C/m^2", + "1 C/cm^2", + "10 C/cm^2", + "1 C/mm^2", + /* default */ "1e+06 C/mm^2"}, + // VolumeChargeDensity + {"1 C/m^3", + "10 C/m^3", + "100 C/m^3", + "1 C/cm^3", + "10 C/cm^3", + "100 C/cm^3", + "1 C/mm^3", + /* default */ "1e+06 C/mm^3"}, + // CurrentDensity + {"1 A/m^2", + "10 A/m^2", + "100 A/m^2", + "1 A/cm^2", + "10 A/cm^2", + "1 A/mm^2", + /* default */ "1e+06 A/mm^2"}, + // ElectricalCapacitance + {"1 pF", + "10 pF", + "100 pF", + "1 nF", + "10 nF", + "100 nF", + "1 \xC2\xB5" + "F", + "10 \xC2\xB5" + "F", + "100 \xC2\xB5" + "F", + "1 mF", + "10 mF", + "100 mF", + "1 F", + /* default */ "1e+06 F"}, + // ElectricalInductance + {"1 nH", + "10 nH", + "100 nH", + "1 \xC2\xB5H", + "10 \xC2\xB5H", + "100 \xC2\xB5H", + "1 mH", + "10 mH", + "100 mH", + "1 H", + /* default */ "1e+06 H"}, + // ElectricalConductance + {"1 \xC2\xB5S", + "10 \xC2\xB5S", + "100 \xC2\xB5S", + "1 mS", + "10 mS", + "100 mS", + "1 S", + /* default */ "1e+06 S"}, + // ElectricalResistance + {"1 Ohm", + "10 Ohm", + "100 Ohm", + "1 kOhm", + "10 kOhm", + "100 kOhm", + "1 MOhm", + /* default */ "1e+06 MOhm"}, + // MagneticFluxDensity + {"1 mT", + "10 mT", + "100 mT", + "1 T", + /* default */ "1e+06 T"}, + // Stiffness + {"1 mN/m", + "10 mN/m", + "100 mN/m", + "1 N/m", + "10 N/m", + "100 N/m", + "1 kN/m", + "10 kN/m", + "100 kN/m", + "1 MN/m", + /* default */ "1e+06 MN/m"}, + // KinematicViscosity + {"1 mm^2/s", + "10 mm^2/s", + "100 mm^2/s", + "1 m^2/s", + /* default */ "1e+06 m^2/s"}, + // VolumeFlowRate + {"1 mm^3/s", + "10 mm^3/s", + "100 mm^3/s", + "1 ml/s", + "10 ml/s", + "100 ml/s", + "1 l/s", + "10 l/s", + "100 l/s", + "1 m^3/s", + /* default */ "1e+06 m^3/s"}, + }); +} + +TEST_F(SchemaTest, sweep_mks) +{ + UnitsApi::setSchema("MKS"); + UnitsApi::setDecimals(6); + sweepCheck({ + // Length + {"1 nm", + "10 nm", + "100 nm", + "1 \xC2\xB5m", + "10 \xC2\xB5m", + "1 mm", + "10 mm", + "100 mm", + "1000 mm", + "10 m", + "100 m", + "1000 m", + "10 km", + "100 km", + "1000 km", + /* default */ "1e+09 m"}, + // Mass + {"1 \xC2\xB5g", + "10 \xC2\xB5g", + "100 \xC2\xB5g", + "1 mg", + "10 mg", + "100 mg", + "1 g", + "10 g", + "100 g", + "1 kg", + "10 kg", + "100 kg", + "1 t", + "10 t", + /* default */ "1e+06 t"}, + // Area + {"1 mm^2", + "10 mm^2", + "1 cm^2", + "10 cm^2", + "100 cm^2", + "1000 cm^2", + "1 m^2", + "10 m^2", + "100 m^2", + "1000 m^2", + "1 km^2", + /* default */ "1e+06 km^2"}, + // Volume + {"1 mm^3", + "10 mm^3", + "100 mm^3", + "1 ml", + "10 ml", + "100 ml", + "1 l", + "10 l", + "100 l", + "1 m^3", + "10 m^3", + /* default */ "1e+06 m^3"}, + // Pressure + {"1 Pa", + "10 Pa", + "100 Pa", + "1000 Pa", + "10 kPa", + "100 kPa", + "1000 kPa", + "10 MPa", + "100 MPa", + "1000 MPa", + "10 GPa", + "100 GPa", + "1000 GPa", + /* default */ "1e+15 Pa"}, + // Force + {"1 mN", + "10 mN", + "100 mN", + "1 N", + "10 N", + "100 N", + "1 kN", + "10 kN", + "100 kN", + "1 MN", + "10 MN", + /* default */ "1e+06 MN"}, + // Power + {"1 mW", + "10 mW", + "100 mW", + "1 W", + "10 W", + "100 W", + "1 kW", + "10 kW", + /* default */ "1e+06 kW"}, + // ElectricPotential + {"1 mV", + "10 mV", + "100 mV", + "1 V", + "10 V", + "100 V", + "1 kV", + "10 kV", + "100 kV", + /* default */ "1e+07 V"}, + // Frequency + {"1 Hz", + "10 Hz", + "100 Hz", + "1 kHz", + "10 kHz", + "100 kHz", + "1 MHz", + "10 MHz", + "100 MHz", + "1 GHz", + "10 GHz", + "100 GHz", + "1 THz", + /* default */ "1e+06 THz"}, + // ThermalConductivity + {"1 W/m/K", + "10 W/m/K", + "100 W/m/K", + "1 W/mm/K", + "10 W/mm/K", + /* default */ "1e+06 W/mm/K"}, + // ElectricalConductivity + {"1 mS/m", + "10 mS/m", + "100 mS/m", + "1 S/m", + "10 S/m", + "100 S/m", + "1 kS/m", + "10 kS/m", + "100 kS/m", + "1 MS/m", + /* default */ "1e+06 MS/m"}, + // CurrentDensity + {"1 A/m^2", + "10 A/m^2", + "1 A/mm^2", + /* default */ "1e+06 A/mm^2"}, + // ElectricalInductance + {"1 nH", + "10 nH", + "100 nH", + "1 \xC2\xB5H", + "10 \xC2\xB5H", + "100 \xC2\xB5H", + "1 mH", + "10 mH", + "100 mH", + "1 H", + /* default */ "1e+06 H"}, + // ElectricalCapacitance + {"1 pF", + "10 pF", + "100 pF", + "1 nF", + "10 nF", + "100 nF", + "1 \xC2\xB5" + "F", + "10 \xC2\xB5" + "F", + "100 \xC2\xB5" + "F", + "1 mF", + "10 mF", + "100 mF", + "1 F", + /* default */ "1e+06 F"}, + }); +} + +TEST_F(SchemaTest, sweep_imperial) +{ + UnitsApi::setSchema("Imperial"); + UnitsApi::setDecimals(6); + sweepCheck({ + // Length + {"1 thou", + "10 thou", + "1\"", + "10\"", + "1'", + "2'", + "1 yd", + "10 yd", + "100 yd", + "1 mi", + /* default */ "1e+09 in"}, + // Pressure + {"1 psi", + "10 psi", + "100 psi", + "1 ksi", + /* default */ "1e+06 psi"}, + }); +} + +TEST_F(SchemaTest, sweep_imperial_decimal) +{ + UnitsApi::setSchema("ImperialDecimal"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 in", "10 in", "100 in"}, + {"1 in^2", "10 in^2", "100 in^2"}, + {"1 in^3", "10 in^3"}, + {"1 lb", "10 lb", "100 lb"}, + {"1 psi", "10 psi", "100 psi"}, + }); +} + +TEST_F(SchemaTest, sweep_imperial_building) +{ + UnitsApi::setSchema("ImperialBuilding"); + UnitsApi::setDecimals(6); + sweepCheck({ + // Length (toFractional) + {"1/8\"", "1/4\"", "3/8\"", "1/2\"", "5/8\"", "3/4\"", "7/8\"", "1\"", "6\"", "1'"}, + // Area, Volume + {"1 sqft", "10 sqft", "100 sqft"}, + {"1 cft", "10 cft", "100 cft"}, + }); +} + +TEST_F(SchemaTest, sweep_imperial_civil) +{ + UnitsApi::setSchema("ImperialCivil"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 ft", "10 ft", "100 ft"}, + {"1 ft^2", "10 ft^2", "100 ft^2"}, + {"1 ft^3", "10 ft^3"}, + {"1 lb", "10 lb", "100 lb"}, + {"1 psi", "10 psi", "100 psi"}, + {"1 mph", "10 mph", "100 mph"}, + // Angle (toDMS) + {"1°", "1°30′", "10°", "10°6′36″", "45°", "45°30′", "90°", "180°", "360°"}, + }); +} + +TEST_F(SchemaTest, sweep_centimeter) +{ + UnitsApi::setSchema("Centimeter"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 cm", "10 cm", "100 cm", "1000 cm"}, + {"1 m^2", "10 m^2", "100 m^2"}, + {"1 m^3", "10 m^3"}, + {"1 W", "10 W", "100 W"}, + {"1 V", "10 V", "100 V"}, + }); +} + +TEST_F(SchemaTest, sweep_fem) +{ + UnitsApi::setSchema("FEM"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 mm", "10 mm", "100 mm", "1000 mm"}, + {"1 t", "10 t", "100 t"}, + }); +} + +TEST_F(SchemaTest, sweep_mmmin) +{ + UnitsApi::setSchema("MmMin"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 mm", "10 mm", "100 mm", "1000 mm"}, + {"1 mm/min", "10 mm/min", "100 mm/min"}, + }); +} + +TEST_F(SchemaTest, sweep_meter_decimal) +{ + UnitsApi::setSchema("MeterDecimal"); + UnitsApi::setDecimals(6); + sweepCheck({ + {"1 m", "10 m", "100 m", "1000 m"}, + {"1 m^2", "10 m^2", "100 m^2"}, + {"1 m^3", "10 m^3"}, + {"1 W", "10 W", "100 W"}, + {"1 V", "10 V", "100 V"}, + {"1 m/s", "10 m/s", "100 m/s"}, + }); +}