// 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 #include #include #include #include #include namespace { using namespace mp_units; using namespace mp_units::utility; using namespace mp_units::si::unit_symbols; // ============================================================================ // Test quantity specifications and origins with bounds // ============================================================================ QUANTITY_SPEC(test_angle_clamp, isq::angular_measure); QUANTITY_SPEC(test_angle_wrap, isq::angular_measure); QUANTITY_SPEC(test_angle_reflect, isq::angular_measure); QUANTITY_SPEC(test_angle_wrap_constrained, isq::angular_measure); QUANTITY_SPEC(test_angle_reflect_constrained, isq::angular_measure); QUANTITY_SPEC(test_angle_unordered_args, isq::angular_measure); struct test_origin_tag final {}; inline constexpr struct clamp_origin final : absolute_point_origin { } clamp_origin; inline constexpr struct wrap_origin final : absolute_point_origin { } wrap_origin; inline constexpr struct reflect_origin final : absolute_point_origin { } reflect_origin; inline constexpr struct wrap_constrained_origin final : absolute_point_origin { } wrap_constrained_origin; inline constexpr struct reflect_constrained_origin final : absolute_point_origin { } reflect_constrained_origin; inline constexpr struct unordered_args_origin final : absolute_point_origin { } unordered_args_origin; // Separate origins for unit conversion tests (need double bounds) QUANTITY_SPEC(test_angle_clamp_convert, isq::angular_measure); QUANTITY_SPEC(test_angle_wrap_convert, isq::angular_measure); inline constexpr struct clamp_convert_origin final : absolute_point_origin { } clamp_convert_origin; inline constexpr struct wrap_convert_origin final : absolute_point_origin { } wrap_convert_origin; // Origins for testing point_for() with bounds // MSL and AGL are independent absolute origins — the translation between them // depends on terrain elevation, which is a runtime value and cannot be fixed // at compile time as a relative_point_origin offset. QUANTITY_SPEC(bounded_altitude, isq::height); inline constexpr struct altitude_msl final : absolute_point_origin { } altitude_msl; // Mean Sea Level: physical bounds [-500m, 12000m] inline constexpr struct altitude_agl final : absolute_point_origin { } altitude_agl; // Above Ground Level: operational bounds [0m, 500m] (drone) // Longitude with absolute origin (prime meridian) and a relative Warsaw origin. QUANTITY_SPEC(geo_longitude, isq::angular_measure); inline constexpr struct prime_meridian final : absolute_point_origin { } prime_meridian; // bounds: [-180°, 180°] // Body and room temperature for point_for() cross-origin bounds tests. QUANTITY_SPEC(clinical_temperature, isq::thermodynamic_temperature); inline constexpr struct body_temp_origin final : absolute_point_origin(35.0), delta(42.0)}> { } body_temp_origin; // bounds: [35°C, 42°C] (clinical range) inline constexpr struct room_temp_origin final : absolute_point_origin(15.0), delta(30.0)}> { } room_temp_origin; // bounds: [15°C, 30°C] // Warsaw meridian: ~21°E of prime meridian. // A quantity_point measured from warsaw_meridian represents "degrees east/west of Warsaw". // No user-specific bounds are added here; only the absolute origin's physical bounds apply. inline constexpr struct warsaw_meridian final : mp_units::relative_point_origin{21.0 * deg, prime_meridian}> { } warsaw_meridian; // A second relative origin further east (e.g., Kyiv ~30°E) for chained-offset tests. inline constexpr struct kyiv_meridian final : mp_units::relative_point_origin{30.0 * deg, prime_meridian}> { } kyiv_meridian; using qp_clamp = quantity_point; using qp_wrap = quantity_point; using qp_reflect = quantity_point; using qp_unordered_args = quantity_point; using qp_clamp_int = quantity_point; // constrained rep: bounds stored as int, incoming value type is constrained // (uses default policy: throw_policy in hosted, terminate_policy in freestanding) using safe_double = constrained; using qp_wrap_constrained = quantity_point; using qp_reflect_constrained = quantity_point; // Type aliases for unit conversion tests (need double bounds) using qp_clamp_convert = quantity_point; using qp_wrap_convert = quantity_point; // ============================================================================ // FILE OVERVIEW // // Part I: Basic policy mechanics // Clamp, wrap, reflect policies; constrained-rep cross-type arithmetic; // mutating operators (+=, -=, ++, --); unit conversion; temperature-unit // semantics (Δ°C = ΔK, °C-style bounds, ice-point via relative origin). // // Part II: Origin-inheritance scenarios // How bounds propagate through the origin chain. Scenarios are classified // by which origin(s) carry bounds: // Scenario 1 abs[bounds] (direct or inherited by rel) // Scenario 2 abs[none] ← rel[bounds] (rel-only bounds) // Scenario 3 abs[bounds] ← rel[bounds] (both; rel is tighter) // Scenario 4 abs[none] ← rel1[bounds] ← rel2[none] (bounds inherited through chain) // Scenario 5 abs[none] ← rel1[bounds] ← rel2[bounds] (two levels of own bounds) // // Part III: Additional domain and edge-case tests // Time-of-day (wrap_to_range on isq::duration), min()/max()/numeric_limits. // ============================================================================ // ============================================================================ // Clamp policy // ============================================================================ static_assert(qp_clamp(45.0 * deg, clamp_origin).quantity_from(clamp_origin) == 45.0 * deg); static_assert(qp_clamp(100.0 * deg, clamp_origin).quantity_from(clamp_origin) == 90.0 * deg); static_assert(qp_clamp(-120.0 * deg, clamp_origin).quantity_from(clamp_origin) == -90.0 * deg); static_assert(qp_clamp(90.0 * deg, clamp_origin).quantity_from(clamp_origin) == 90.0 * deg); static_assert(qp_clamp(-90.0 * deg, clamp_origin).quantity_from(clamp_origin) == -90.0 * deg); // ============================================================================ // Wrap policy // ============================================================================ static_assert(qp_wrap(90.0 * deg, wrap_origin).quantity_from(wrap_origin) == 90.0 * deg); static_assert(qp_wrap(200.0 * deg, wrap_origin).quantity_from(wrap_origin) == -160.0 * deg); static_assert(qp_wrap(-200.0 * deg, wrap_origin).quantity_from(wrap_origin) == 160.0 * deg); static_assert(qp_wrap(180.0 * deg, wrap_origin).quantity_from(wrap_origin) == -180.0 * deg); // ============================================================================ // Reflect policy // ============================================================================ static_assert(qp_reflect(45.0 * deg, reflect_origin).quantity_from(reflect_origin) == 45.0 * deg); static_assert(qp_reflect(91.0 * deg, reflect_origin).quantity_from(reflect_origin) == 89.0 * deg); // ============================================================================ // Bounds argument does not require first position in NTTP pack // ============================================================================ static_assert(qp_unordered_args(50.0 * deg, unordered_args_origin).quantity_from(unordered_args_origin) == 45.0 * deg); static_assert(qp_unordered_args(-50.0 * deg, unordered_args_origin).quantity_from(unordered_args_origin) == -45.0 * deg); static_assert(qp_reflect(-91.0 * deg, reflect_origin).quantity_from(reflect_origin) == -89.0 * deg); static_assert(qp_reflect(90.0 * deg, reflect_origin).quantity_from(reflect_origin) == 90.0 * deg); static_assert(qp_reflect(-90.0 * deg, reflect_origin).quantity_from(reflect_origin) == -90.0 * deg); // ============================================================================ // wrap/reflect with constrained rep (bounds stored as int, incoming rep is constrained) // This exercises the cross-type arithmetic in wrap_to_range::operator() and // reflect_in_range::operator(). // ============================================================================ static_assert(qp_wrap_constrained(safe_double{90.0} * deg, wrap_constrained_origin) .quantity_from(wrap_constrained_origin) == safe_double{90.0} * deg); static_assert(qp_wrap_constrained(safe_double{200.0} * deg, wrap_constrained_origin) .quantity_from(wrap_constrained_origin) == safe_double{-160.0} * deg); static_assert(qp_wrap_constrained(safe_double{-200.0} * deg, wrap_constrained_origin) .quantity_from(wrap_constrained_origin) == safe_double{160.0} * deg); static_assert(qp_reflect_constrained(safe_double{45.0} * deg, reflect_constrained_origin) .quantity_from(reflect_constrained_origin) == safe_double{45.0} * deg); static_assert(qp_reflect_constrained(safe_double{91.0} * deg, reflect_constrained_origin) .quantity_from(reflect_constrained_origin) == safe_double{89.0} * deg); static_assert(qp_reflect_constrained(safe_double{-91.0} * deg, reflect_constrained_origin) .quantity_from(reflect_constrained_origin) == safe_double{-89.0} * deg); // ============================================================================ // Mutating operators enforce bounds // ============================================================================ consteval bool clamp_plus_assign() { auto pt = qp_clamp(80.0 * deg, clamp_origin); pt += 20.0 * deg; return pt.quantity_from(clamp_origin) == 90.0 * deg; } static_assert(clamp_plus_assign()); consteval bool clamp_minus_assign() { auto pt = qp_clamp(-80.0 * deg, clamp_origin); pt -= 20.0 * deg; return pt.quantity_from(clamp_origin) == -90.0 * deg; } static_assert(clamp_minus_assign()); consteval bool clamp_pre_increment() { auto pt = qp_clamp_int(90 * deg, clamp_origin); ++pt; return pt.quantity_from(clamp_origin) == 90 * deg; } static_assert(clamp_pre_increment()); consteval bool clamp_pre_decrement() { auto pt = qp_clamp_int(-90 * deg, clamp_origin); --pt; return pt.quantity_from(clamp_origin) == -90 * deg; } static_assert(clamp_pre_decrement()); consteval bool clamp_post_increment() { auto pt = qp_clamp_int(89 * deg, clamp_origin); auto old_value = pt++; // Check old value returned if (old_value.quantity_from(clamp_origin) != 89 * deg) return false; // Check new value is clamped to max return pt.quantity_from(clamp_origin) == 90 * deg; } static_assert(clamp_post_increment()); consteval bool clamp_post_decrement() { auto pt = qp_clamp_int(-89 * deg, clamp_origin); auto old_value = pt--; // Check old value returned if (old_value.quantity_from(clamp_origin) != -89 * deg) return false; // Check new value is clamped to min return pt.quantity_from(clamp_origin) == -90 * deg; } static_assert(clamp_post_decrement()); consteval bool wrap_post_increment() { auto pt_wrap_int = quantity_point(179 * deg, wrap_origin); auto old_value = pt_wrap_int++; // Check old value returned if (old_value.quantity_from(wrap_origin) != 179 * deg) return false; // Check new value wraps to -180 (since range is [-180, 180)) return pt_wrap_int.quantity_from(wrap_origin) == -180 * deg; } static_assert(wrap_post_increment()); // ============================================================================ // Unit conversion with bounded quantity_points // ============================================================================ // Simple scaling: degrees to radians and back consteval bool angle_unit_conversion() { auto pt = qp_clamp_convert(45.0 * deg, clamp_convert_origin); auto in_rad = pt.in(rad); // 45° in radians constexpr auto expected_rad = (45.0 * deg).numerical_value_in(rad); return in_rad.quantity_from(clamp_convert_origin).numerical_value_in(rad) == expected_rad; } static_assert(angle_unit_conversion()); consteval bool clamp_with_unit_scaling() { // Create with radians (exceeds bounds in degrees: π rad = 180°), should clamp to 90° constexpr auto pi_rad = 1.0 * pi * rad; auto pt = qp_clamp_convert(pi_rad, clamp_convert_origin); // Expected: clamped to 90° constexpr auto expected = 90.0 * deg; return pt.quantity_from(clamp_convert_origin) == expected; } static_assert(clamp_with_unit_scaling()); // Wrap with unit scaling consteval bool wrap_with_unit_scaling() { // 400° wraps to 40° (400 - 360 = 40) auto pt = qp_wrap_convert(400.0 * deg, wrap_convert_origin); const auto in_rad = pt.in(rad); const auto back_in_deg = in_rad.quantity_from(wrap_convert_origin); // Wrapped value: 400 - 360 = 40, which is in [-180, 180) return back_in_deg == 40.0 * deg; } static_assert(wrap_with_unit_scaling()); // ============================================================================ // Temperature conversion with offset units // ============================================================================ // Temperature origin with bounds in kelvin QUANTITY_SPEC(bounded_temperature, isq::thermodynamic_temperature); inline constexpr struct temp_origin final : absolute_point_origin(200.0), delta(400.0)}> { } temp_origin; // Relative origin at +300K above temp_origin (simulates an ice-point-style offset). // 300K is within temp_origin's [200K, 400K] range, so the QP constructor does not clamp it. // Has NO own bounds; inherits enforcement from temp_origin's [200K, 400K] range. // Local range from temp_offset_origin: [200-300, 400-300] = [-100K, 100K]. inline constexpr struct temp_offset_origin final : mp_units::relative_point_origin{ delta(300.0), temp_origin}> { } temp_offset_origin; using qp_temp = quantity_point; // Value within bounds static_assert(qp_temp(delta(300.0), temp_origin).quantity_from(temp_origin) == delta(300.0)); // Above max clamps static_assert(qp_temp(delta(500.0), temp_origin).quantity_from(temp_origin) == delta(400.0)); // Below min clamps static_assert(qp_temp(delta(100.0), temp_origin).quantity_from(temp_origin) == delta(200.0)); // Convert to millikelvin (simple scaling) consteval bool temperature_unit_scaling() { auto pt = qp_temp(delta(300.0), temp_origin); auto in_mK = pt.in(si::milli); // 300 K = 300'000 mK return in_mK.quantity_from(temp_origin).numerical_value_in(si::milli) == 300'000.0; } static_assert(temperature_unit_scaling()); // Creating with millikelvin (out of bounds) clamps correctly consteval bool temperature_clamp_with_millikelvin() { // 500'000 mK = 500 K, which exceeds max of 400 K auto pt = qp_temp(delta>(500'000.0), temp_origin); // Should clamp to 400 K = 400'000 mK return pt.in(si::milli).quantity_from(temp_origin) == delta>(400'000.0); } static_assert(temperature_clamp_with_millikelvin()); // ============================================================================ // Offset-origin bounds: bounds on the parent absolute origin are applied after // translating by the +300K offset of temp_offset_origin. // Local allowed range: [200-300, 400-300] = [-100K, 100K] from temp_offset_origin. // This also verifies that deg_C input (Δ°C == ΔK, same scale) produces the same // result — confirming that the "offset" in bounds comes from the origin chain, not // from the unit's own offset. // ============================================================================ using qp_offset = quantity_point; // Within absolute bounds: 0K from offset = 300K abs ∈ [200, 400] → no clamp. static_assert(qp_offset(delta(0.0), temp_offset_origin) .quantity_from(temp_offset_origin) == delta(0.0)); // Below abs min: -200K from offset = 100K abs < 200K → clamp to 200K abs → -100K from offset. static_assert(qp_offset(delta(-200.0), temp_offset_origin) .quantity_from(temp_offset_origin) == delta(-100.0)); // Above abs max: 200K from offset = 500K abs > 400K → clamp to 400K abs → 100K from offset. static_assert(qp_offset(delta(200.0), temp_offset_origin) .quantity_from(temp_offset_origin) == delta(100.0)); // Same tests using deg_C as input unit (Δ°C = ΔK, 1:1 scale): // confirms that specifying the value in deg_C rather than K does not introduce // a spurious 273.15 shift — the offset lives in the origin, not in the delta unit. static_assert(qp_offset(delta(-200.0), temp_offset_origin) .quantity_from(temp_offset_origin) == delta(-100.0)); static_assert(qp_offset(delta(200.0), temp_offset_origin) .quantity_from(temp_offset_origin) == delta(100.0)); // ============================================================================ // Bounds in °C, input/output in K: point-temperature shift lives in the origin. // // A new spec is given bounds expressed in degrees Celsius [200 Δ°C, 350 Δ°C]. // Because Δ°C == ΔK (same scale), these are numerically identical to [200 ΔK, 350 ΔK]. // So bounds in °C give EXACTLY the same clamp thresholds as bounds in K for any // input unit — there is no automatic "add-273" treatment. // // To get the physical ice-point shift (273 K) one must use a RELATIVE ORIGIN // placed 273 K above the absolute origin. The same numerical K value submitted // to each origin then receives different treatment (accepted vs clamped), // because it represents a different absolute temperature in each frame. // // celsius_style_abs_origin bounds [200 Δ°C, 350 Δ°C] ≡ [200 ΔK, 350 ΔK] // celsius_style_ice_origin at +273 K from abs_origin (no own bounds) // inherited range: [200-273, 350-273] = [-73 K, +77 K] // ============================================================================ QUANTITY_SPEC(celsius_style_temp, isq::thermodynamic_temperature); inline constexpr struct celsius_style_abs_origin final : absolute_point_origin(200.0), delta(350.0)}> { } celsius_style_abs_origin; // Relative origin at +273 K: simulates the ice-point shift with a round number. // 273 ∈ [200, 350] so the QP constructor does not clamp it. // Inherits abs_origin's bounds; local allowed range: [-73 K, +77 K]. inline constexpr struct celsius_style_ice_origin final : mp_units::relative_point_origin< mp_units::quantity_point{ delta(273.0), celsius_style_abs_origin}> { } celsius_style_ice_origin; using qp_abs = quantity_point; using qp_ice = quantity_point; // --- Bounds in °C, input in K: Δ°C == ΔK, no 273 shift from the unit --- // Within [200 Δ°C, 350 Δ°C] = [200 ΔK, 350 ΔK]: 270 K passes. static_assert(qp_abs(delta(270.0), celsius_style_abs_origin) .quantity_from(celsius_style_abs_origin) == delta(270.0)); // Below 200 Δ°C (= 200 ΔK): 30 K → clamped to 200 K, NOT to 473 K (200+273). static_assert(qp_abs(delta(30.0), celsius_style_abs_origin) .quantity_from(celsius_style_abs_origin) == delta(200.0)); // Same with °C input: identical result confirms Δ°C == ΔK in bound comparisons. static_assert(qp_abs(delta(30.0), celsius_style_abs_origin) .quantity_from(celsius_style_abs_origin) == delta(200.0)); // Above 350 Δ°C (= 350 ΔK): 400 K → clamped to 350 K. static_assert(qp_abs(delta(400.0), celsius_style_abs_origin) .quantity_from(celsius_style_abs_origin) == delta(350.0)); // .in(K) is a unit-label change only: stored value is unchanged, no 273 shift. consteval bool celsius_style_in_kelvin() { auto pt = qp_abs(delta(270.0), celsius_style_abs_origin); auto in_K = pt.in(si::kelvin); return in_K.quantity_from(celsius_style_abs_origin).numerical_value_in(si::kelvin) == 270.0; } static_assert(celsius_style_in_kelvin()); // --- The 273 K shift lives in the origin, not the unit --- // // The SAME numerical value "30 K": // • from celsius_style_abs_origin → absolute = 30 K → below 200 → CLAMPED to 200 K // • from celsius_style_ice_origin → absolute = 273+30 = 303 K → within [200, 350] → VALID // 30 K from abs_origin — below min — clamped (shown above; repeated for side-by-side clarity). static_assert(qp_abs(delta(30.0), celsius_style_abs_origin) .quantity_from(celsius_style_abs_origin) == delta(200.0)); // 30 K from ice_origin — absolute 303 K — within [200, 350] — NOT clamped. static_assert(qp_ice(delta(30.0), celsius_style_ice_origin) .quantity_from(celsius_style_ice_origin) == delta(30.0)); // Above max from ice_origin: 100 K → absolute 373 K > 350 → clamp → 350-273 = 77 K. static_assert(qp_ice(delta(100.0), celsius_style_ice_origin) .quantity_from(celsius_style_ice_origin) == delta(77.0)); // Below min from ice_origin: -300 K → absolute -27 K < 200 → clamp → 200-273 = -73 K. static_assert(qp_ice(delta(-300.0), celsius_style_ice_origin) .quantity_from(celsius_style_ice_origin) == delta(-73.0)); // ============================================================================ // Conversions between different origins (with offsets) // ============================================================================ // Test conversion between absolute_zero and ice_point origins // ice_point is at 273.15K above absolute_zero consteval bool convert_between_offset_origins() { // Create a bounded point at temp_origin: 300K auto pt_temp = qp_temp(delta(300.0), temp_origin); // Convert to quantity_point with si::absolute_zero origin // This requires going through the quantity: quantity_from gives us the displacement const auto q = pt_temp.quantity_from(temp_origin); auto pt_abs_zero = si::absolute_zero + q; // Now convert to ice_point (which is 273.15K above absolute_zero) const auto q_from_ice = pt_abs_zero.quantity_from(si::ice_point); // 300K from absolute_zero = (300 - 273.15)K from ice_point = 26.85K // Use approximate comparison due to floating point precision constexpr auto expected = 300.0 - 273.15; const auto actual = q_from_ice.numerical_value_in(si::kelvin); return (actual >= expected - 0.0001) && (actual <= expected + 0.0001); } static_assert(convert_between_offset_origins()); // ============================================================================ // Scenario 1: Only absolute bounds (absolute origin WITH bounds). // // A bounds specialization lives directly on an absolute_point_origin. // Relative origins that build on top carry no own bounds and simply inherit // the absolute origin's enforcement (cumulative offset translated away first). // // Examples below: // altitude_msl / altitude_agl — two independent absolute origins // prime_meridian / warsaw / kyiv — absolute bounds + inheriting relative origins // body_temp_origin / room_temp — two independent absolute origins (°C) // check_origin — absolute origin with check_in_range policy // ============================================================================ // ============================================================================ // Altitude: two independent absolute origins (MSL and AGL). // The translation between them is terrain-dependent and must be done at runtime // by the user; the library cannot express it as a relative_point_origin offset. // Each origin enforces its own physical or operational bounds independently. // ============================================================================ using qp_altitude_msl = quantity_point; using qp_altitude_agl = quantity_point; // MSL: value within bounds. static_assert(qp_altitude_msl(1000.0 * m, altitude_msl).quantity_from(altitude_msl) == 1000.0 * m); // MSL: above max clamps to 12000m. static_assert(qp_altitude_msl(15000.0 * m, altitude_msl).quantity_from(altitude_msl) == 12000.0 * m); // AGL: value within bounds. static_assert(qp_altitude_agl(200.0 * m, altitude_agl).quantity_from(altitude_agl) == 200.0 * m); // AGL: above max clamps to 500m. static_assert(qp_altitude_agl(700.0 * m, altitude_agl).quantity_from(altitude_agl) == 500.0 * m); // AGL: below min (negative AGL is underground) clamps to 0m. static_assert(qp_altitude_agl(-10.0 * m, altitude_agl).quantity_from(altitude_agl) == 0.0 * m); // ============================================================================ // Longitude: relative origin (Warsaw) inherits absolute physical bounds. // No user-specific bounds on warsaw_meridian — the absolute origin's [-180°,180°] // bounds are always enforced (translated by the +21° offset). // Max allowed from Warsaw = 180° - 21° = 159°. // Min allowed from Warsaw = -180° - 21° = -201°. // ============================================================================ using qp_prime = quantity_point; using qp_warsaw = quantity_point; using qp_kyiv = quantity_point; // Prime meridian: value within bounds. static_assert(qp_prime(90.0 * deg, prime_meridian).quantity_from(prime_meridian) == 90.0 * deg); // Prime meridian: above 180° clamps to 180°. static_assert(qp_prime(200.0 * deg, prime_meridian).quantity_from(prime_meridian) == 180.0 * deg); // Warsaw: value within absolute bounds (21° + 100° = 121°, within [-180°, 180°]). static_assert(qp_warsaw(100.0 * deg, warsaw_meridian).quantity_from(warsaw_meridian) == 100.0 * deg); // Warsaw: 200° east of Warsaw = 221° from prime meridian → exceeds 180° → clamps. // After clamp in absolute frame: 180°. Back in Warsaw frame: 180° - 21° = 159°. static_assert(qp_warsaw(200.0 * deg, warsaw_meridian).quantity_from(warsaw_meridian) == 159.0 * deg); // Warsaw: -200° west of Warsaw = -179° from prime meridian → within [-180°, 180°] → no clamp. static_assert(qp_warsaw(-170.0 * deg, warsaw_meridian).quantity_from(warsaw_meridian) == -170.0 * deg); // Warsaw: -165° west of Warsaw = -144° from prime meridian → no clamp; straightforward. static_assert(qp_warsaw(-165.0 * deg, warsaw_meridian).quantity_from(warsaw_meridian) == -165.0 * deg); // Kyiv relative origin: same physical bounds check. 170° east of Kyiv = 200° → clamps. // 200° from prime → clamps to 180° → 180° - 30° = 150° from Kyiv. static_assert(qp_kyiv(170.0 * deg, kyiv_meridian).quantity_from(kyiv_meridian) == 150.0 * deg); // ============================================================================ // point_for() cross-origin bounds: body vs room temperature. // A body temperature (37°C) is valid for body_temp_origin, // but out-of-range for room_temp_origin [15°C, 30°C] — should clamp on conversion. // Since these are independent absolute origins there is no point_for() between them; // the user must do the conversion explicitly via quantity_from. // The following tests validate that each absolute origin clamps independently. // ============================================================================ using qp_body = quantity_point; using qp_room = quantity_point; // Body temperature 37°C: within [35, 42] → unchanged. static_assert(qp_body(delta(37.0), body_temp_origin) .quantity_from(body_temp_origin) == delta(37.0)); // Body temperature 44°C: above max 42°C → clamps to 42°C. static_assert(qp_body(delta(44.0), body_temp_origin) .quantity_from(body_temp_origin) == delta(42.0)); // Room temperature 20°C: within [15, 30] → unchanged. static_assert(qp_room(delta(20.0), room_temp_origin) .quantity_from(room_temp_origin) == delta(20.0)); // Room temperature 37°C (a body temperature entered as room): exceeds [15, 30] → clamps to 30°C. static_assert(qp_room(delta(37.0), room_temp_origin) .quantity_from(room_temp_origin) == delta(30.0)); // ============================================================================ // Scenario 1 (continued): check_in_range policy. // Same abs[bounds] topology; behaviour on out-of-range values is a contract // violation (MP_UNITS_EXPECTS) rather than a silent clamp/wrap/reflect. // Only in-range paths can be tested at compile time. // ============================================================================ // Origin with check_in_range policy QUANTITY_SPEC(test_angle_check, isq::angular_measure); inline constexpr struct check_origin final : absolute_point_origin { } check_origin; using qp_check = quantity_point; // Values within bounds should work fine static_assert(qp_check(45.0 * deg, check_origin).quantity_from(check_origin) == 45.0 * deg); static_assert(qp_check(0.0 * deg, check_origin).quantity_from(check_origin) == 0.0 * deg); static_assert(qp_check(90.0 * deg, check_origin).quantity_from(check_origin) == 90.0 * deg); static_assert(qp_check(-90.0 * deg, check_origin).quantity_from(check_origin) == -90.0 * deg); // Arithmetic within bounds consteval bool check_arithmetic_within_bounds() { auto pt = qp_check(45.0 * deg, check_origin); pt += 30.0 * deg; // Result: 75°, within bounds return pt.quantity_from(check_origin) == 75.0 * deg; } static_assert(check_arithmetic_within_bounds()); consteval bool check_increment_within_bounds() { using qp_check_int = quantity_point; auto pt = qp_check_int(45 * deg, check_origin); ++pt; // Result: 46°, within bounds return pt.quantity_from(check_origin) == 46 * deg; } static_assert(check_increment_within_bounds()); // Note: Out-of-bounds cases for check_in_range with plain rep cannot be tested at // compile time since they use MP_UNITS_EXPECTS (may be disabled in release builds). // ============================================================================ // Scenario 2: Only relative bounds (parent absolute origin has NO bounds). // A relative origin defines local operational constraints; the parent origin // has no universal physical invariant. // ============================================================================ QUANTITY_SPEC(mission_range, isq::length); inline constexpr struct home_base final : absolute_point_origin { } home_base; // no bounds — the home base is an unconstrained reference // relative origin: at 100m from home_base, with its own [-20m, 20m] operational bounds. inline constexpr struct patrol_radius final : relative_point_origin { } patrol_radius; using qp_patrol = quantity_point; // Within patrol bounds: unchanged. static_assert(qp_patrol(10.0 * m, patrol_radius).quantity_from(patrol_radius) == 10.0 * m); // Exceeds patrol max (20m): clamps to 20m. static_assert(qp_patrol(30.0 * m, patrol_radius).quantity_from(patrol_radius) == 20.0 * m); // Below patrol min (-20m): clamps to -20m. static_assert(qp_patrol(-25.0 * m, patrol_radius).quantity_from(patrol_radius) == -20.0 * m); // ============================================================================ // Scenario 3: Absolute bounds AND relative bounds (both enforced, relative is tighter). // PO = rel[±5m], parent = abs[±10m]. Only the tighter relative bounds fire at runtime. // A compile-time check verifies relative ⊆ absolute. // ============================================================================ QUANTITY_SPEC(nested_height, isq::height); inline constexpr struct ground_abs final : absolute_point_origin { } ground_abs; // bounds: [-10m, 10m] inline constexpr struct platform final : mp_units::relative_point_origin{ 0.0 * mp_units::si::metre, ground_abs}, mp_units::clamp_to_range{-5.0 * mp_units::si::metre, 5.0 * mp_units::si::metre}> { } platform; using qp_platform = quantity_point; // Within the relative bounds: unchanged. static_assert(qp_platform(3.0 * m, platform).quantity_from(platform) == 3.0 * m); // Exceeds relative max (5m) but within absolute (10m): clamps to relative max 5m. static_assert(qp_platform(7.0 * m, platform).quantity_from(platform) == 5.0 * m); // ============================================================================ // Scenario 4: abs[bounds] ← rel1[bounds] ← rel2[no-bounds]. // rel2 has no own bounds; it inherits rel1's bounds (translated). // rel1 is at offset +5m from abs, with bounds [-3m, +3m]. // rel2 is at offset +1m from rel1 (i.e., +6m from abs), with no own bounds. // Absolute frame: rel2 value v_abs = v_rel2 + 1 (rel2→rel1) + 5 (rel1→abs) // = v_rel2 + 6. // rel1 bounds in absolute frame: [-3+5, +3+5] = [+2, +8]. // rel1 bounds in rel2's frame: [+2-6, +8-6] = [-4, +2]. // So a value of v_rel2 in [-4, +2] passes; outside is clamped. // ============================================================================ QUANTITY_SPEC(chain_length, isq::length); inline constexpr struct chain_abs final : absolute_point_origin { } chain_abs; // bounds: not used directly; testing via rel1 bounds // chain_rel1: at +5m from chain_abs, with bounds [-3m, +3m]. inline constexpr struct chain_rel1 final : mp_units::relative_point_origin{ 5.0 * mp_units::si::metre, chain_abs}, mp_units::clamp_to_range{-3.0 * mp_units::si::metre, 3.0 * mp_units::si::metre}> { } chain_rel1; // chain_rel2: at +1m from chain_rel1 (i.e., +6m from chain_abs), with NO bounds. inline constexpr struct chain_rel2 final : mp_units::relative_point_origin{ 1.0 * mp_units::si::metre, chain_rel1}> { } chain_rel2; using qp_chain2 = quantity_point; // In rel2 frame: 0m → abs frame: 0+1+5=6m → rel1 frame: 6-5=1m → within [-3,3] → pass. // No clamp: 0m from rel2 stays 0m. static_assert(qp_chain2(0.0 * m, chain_rel2).quantity_from(chain_rel2) == 0.0 * m); // In rel2 frame: -3m → abs frame: -3+6=3m → rel1 frame: 3-5=-2m → within [-3,3] → pass. static_assert(qp_chain2(-3.0 * m, chain_rel2).quantity_from(chain_rel2) == -3.0 * m); // In rel2 frame: +3m → abs frame: 3+6=9m → rel1 frame: 9-5=4m → exceeds max 3m → clamp to 3m (rel1). // Back to rel2 frame: 3 - 1 = 2m (offset is 1m from rel1 to rel2; rel1_frame_val - 0 = 3, rel2 = rel1_val - 1 = 2). // rel1 clamps to 3m (in rel1 frame). rel2 val = clamped_rel1 - offset = 3 - 1 = 2. static_assert(qp_chain2(3.0 * m, chain_rel2).quantity_from(chain_rel2) == 2.0 * m); // In rel2 frame: -5m → abs frame: -5+6=1m → rel1 frame: 1-5=-4m → below min -3m → clamp to -3m (rel1). // Back to rel2: -3 - 1 = -4m. static_assert(qp_chain2(-5.0 * m, chain_rel2).quantity_from(chain_rel2) == -4.0 * m); // ============================================================================ // Scenario 5: abs[no-bounds] ← rel1[bounds] ← rel2[bounds]. // rel1 is at +10m from abs (unbounded), with bounds [-5m, +5m]. // rel2 is at +2m from rel1, with its own tighter bounds [-1m, +1m]. // Since rel2 HAS bounds, they are enforced (the tighter ones). // A compile-time check verifies rel2 bounds ⊆ rel1 bounds (in rel1's frame). // rel2 bounds in rel1 frame: [-1+2, +1+2] = [+1, +3] ⊆ [-5, +5] → OK. // ============================================================================ QUANTITY_SPEC(nested2_length, isq::length); inline constexpr struct nested2_abs final : absolute_point_origin { } nested2_abs; // no bounds // nested2_rel1: at +10m from nested2_abs, bounds [-5m, +5m]. inline constexpr struct nested2_rel1 final : mp_units::relative_point_origin{ 10.0 * mp_units::si::metre, nested2_abs}, mp_units::clamp_to_range{-5.0 * mp_units::si::metre, 5.0 * mp_units::si::metre}> { } nested2_rel1; // nested2_rel2: at +2m from nested2_rel1, bounds [-1m, +1m]. inline constexpr struct nested2_rel2 final : mp_units::relative_point_origin{ 2.0 * mp_units::si::metre, nested2_rel1}, mp_units::clamp_to_range{-1.0 * mp_units::si::metre, 1.0 * mp_units::si::metre}> { } nested2_rel2; using qp_nested2 = quantity_point; // Within rel2 bounds [-1, +1]: unchanged. static_assert(qp_nested2(0.5 * m, nested2_rel2).quantity_from(nested2_rel2) == 0.5 * m); // Exceeds rel2 max (+1m): clamp to +1m (rel2's own bounds fire, not rel1's). static_assert(qp_nested2(3.0 * m, nested2_rel2).quantity_from(nested2_rel2) == 1.0 * m); // Below rel2 min (-1m): clamp to -1m. static_assert(qp_nested2(-2.0 * m, nested2_rel2).quantity_from(nested2_rel2) == -1.0 * m); // ============================================================================ // Scenario: time-of-day (wrap_to_range with isq::duration, [0 h, 24 h)) // Models a clock/timestamp that wraps cyclically over a 24-hour day. // This tests wrap_to_range with a duration quantity spec, separate from the // angular tests above. // ============================================================================ QUANTITY_SPEC(time_of_day_qs, isq::duration); inline constexpr struct midnight final : absolute_point_origin { } midnight; using time_of_day = quantity_point; // Within [0, 86400): unchanged. static_assert(time_of_day(0.0 * s, midnight).quantity_from(midnight) == 0.0 * s); static_assert(time_of_day(3600.0 * s, midnight).quantity_from(midnight) == 3600.0 * s); // 01:00 static_assert(time_of_day(43200.0 * s, midnight).quantity_from(midnight) == 43200.0 * s); // 12:00 static_assert(time_of_day(86399.0 * s, midnight).quantity_from(midnight) == 86399.0 * s); // 23:59:59 // Overflow past midnight wraps back to early morning. static_assert(time_of_day(86400.0 * s, midnight).quantity_from(midnight) == 0.0 * s); // 24:00 -> 00:00 static_assert(time_of_day(90000.0 * s, midnight).quantity_from(midnight) == 3600.0 * s); // 25:00 -> 01:00 // Negative values (before midnight) wrap to previous day. static_assert(time_of_day(-3600.0 * s, midnight).quantity_from(midnight) == 82800.0 * s); // -1 h -> 23:00 // Arithmetic: adding a duration that crosses midnight wraps correctly. consteval bool time_of_day_wrap_arithmetic() { auto tod = time_of_day(82800.0 * s, midnight); // 23:00 tod += 7200.0 * s; // +2 h -> 01:00 next day return tod.quantity_from(midnight) == 3600.0 * s; } static_assert(time_of_day_wrap_arithmetic()); // ---- Same tests using hours as the input unit ------------------------------ // Use time_of_day_qs[h] so the quantity spec matches and unit conversion // to the stored unit (s) is handled automatically. // 0 h = 0 s: at lower boundary. static_assert(time_of_day(0.0 * time_of_day_qs[h], midnight).quantity_from(midnight) == 0.0 * s); // 1 h = 3600 s: well inside range. static_assert(time_of_day(1.0 * time_of_day_qs[h], midnight).quantity_from(midnight) == 3600.0 * s); // 24 h = 86400 s: exactly at the upper (exclusive) boundary → wraps to 0. static_assert(time_of_day(24.0 * time_of_day_qs[h], midnight).quantity_from(midnight) == 0.0 * s); // 25 h = 90000 s: one hour past midnight → wraps to 1 h = 3600 s. static_assert(time_of_day(25.0 * time_of_day_qs[h], midnight).quantity_from(midnight) == 3600.0 * s); // -1 h = -3600 s: one hour before midnight → wraps to 23 h = 82800 s. static_assert(time_of_day(-1.0 * time_of_day_qs[h], midnight).quantity_from(midnight) == 82800.0 * s); // ---- Same tests using minutes as the input unit ---------------------------- // 60 min = 3600 s = 1 h. static_assert(time_of_day(60.0 * time_of_day_qs[min], midnight).quantity_from(midnight) == 3600.0 * s); // 1440 min = 86400 s = 24 h → wraps to 0. static_assert(time_of_day(1440.0 * time_of_day_qs[min], midnight).quantity_from(midnight) == 0.0 * s); // -60 min = -3600 s → wraps to 82800 s = 23 h. static_assert(time_of_day(-60.0 * time_of_day_qs[min], midnight).quantity_from(midnight) == 82800.0 * s); // 90 min = 5400 s = 1 h 30 min: inside range. static_assert(time_of_day(90.0 * time_of_day_qs[min], midnight).quantity_from(midnight) == 5400.0 * s); // ---- Same tests using milliseconds as the input unit ----------------------- // 3'600'000 ms = 3600 s = 1 h. static_assert(time_of_day(3'600'000.0 * time_of_day_qs[ms], midnight).quantity_from(midnight) == 3600.0 * s); // 86'400'000 ms = 86400 s → wraps to 0. static_assert(time_of_day(86'400'000.0 * time_of_day_qs[ms], midnight).quantity_from(midnight) == 0.0 * s); // 90'000'000 ms = 90000 s = 25 h → wraps to 3600 s. static_assert(time_of_day(90'000'000.0 * time_of_day_qs[ms], midnight).quantity_from(midnight) == 3600.0 * s); // -3'600'000 ms = -3600 s → wraps to 82800 s. static_assert(time_of_day(-3'600'000.0 * time_of_day_qs[ms], midnight).quantity_from(midnight) == 82800.0 * s); // ---- Cross-unit arithmetic: increment in hours, query in seconds ----------- consteval bool time_of_day_hour_increment() { auto tod = time_of_day(23.0 * time_of_day_qs[h], midnight); // 23:00 = 82800 s tod += 2.0 * time_of_day_qs[h]; // +2 h → 25:00 = 90000 s → wraps to 3600 s return tod.quantity_from(midnight) == 3600.0 * s; } static_assert(time_of_day_hour_increment()); consteval bool time_of_day_minute_increment() { auto tod = time_of_day(23.0 * time_of_day_qs[h] + 30.0 * time_of_day_qs[min], midnight); // 23:30 = 84600 s tod += 45.0 * time_of_day_qs[min]; // +45 min → 24:15 = 87300 s → wraps to 900 s return tod.quantity_from(midnight) == 900.0 * s; } static_assert(time_of_day_minute_increment()); } // namespace // ============================================================================ // Static member functions: min(), max(), and std::numeric_limits // Tests cover: full bounds, half-line min-only, half-line max-only. // ============================================================================ namespace { QUANTITY_SPEC(test_halfbounded, isq::height); // Half-line policy: clamp from below only (has .min, no .max). template struct clamp_min_only { Q min; template constexpr V operator()(V v) const { if (v < V{min}) return V{min}; return v; } }; #if MP_UNITS_COMP_CLANG && MP_UNITS_COMP_CLANG < 17 template clamp_min_only(Q) -> clamp_min_only; #endif // Half-line policy: clamp from above only (has .max, no .min). template struct clamp_max_only { Q max; template constexpr V operator()(V v) const { if (v > V{max}) return V{max}; return v; } }; #if MP_UNITS_COMP_CLANG && MP_UNITS_COMP_CLANG < 17 template clamp_max_only(Q) -> clamp_max_only; #endif // min-only origin: value >= 0 m, no upper bound. inline constexpr struct halfbound_min_origin final : absolute_point_origin { } halfbound_min_origin; // max-only origin: value <= 12000 m, no lower bound. inline constexpr struct halfbound_max_origin final : absolute_point_origin { } halfbound_max_origin; // ---- Full bounds (clamp_origin: [-90 deg, +90 deg]) ------------------------ // quantity_point::min() / max() use the bounds directly. static_assert(qp_clamp::min().quantity_from(clamp_origin) == -90.0 * deg); static_assert(qp_clamp::max().quantity_from(clamp_origin) == 90.0 * deg); // std::numeric_limits delegates to the bounded min/max. static_assert(std::numeric_limits::min().quantity_from(clamp_origin) == -90.0 * deg); static_assert(std::numeric_limits::max().quantity_from(clamp_origin) == 90.0 * deg); // lowest() delegates to min() because .min exists in bounds. static_assert(std::numeric_limits::lowest().quantity_from(clamp_origin) == -90.0 * deg); // ---- Half-line min-only (halfbound_min_origin: value >= 0 m) --------------- using qp_hmin = quantity_point; // min() returns the bound; max() falls back to the representation maximum. static_assert(qp_hmin::min().quantity_from(halfbound_min_origin) == 0.0 * m); static_assert(qp_hmin::max().quantity_from(halfbound_min_origin).numerical_value_in(m) == std::numeric_limits::max()); // numeric_limits mirrors the above. static_assert(std::numeric_limits::min().quantity_from(halfbound_min_origin) == 0.0 * m); static_assert(std::numeric_limits::max().quantity_from(halfbound_min_origin).numerical_value_in(m) == std::numeric_limits::max()); // lowest() delegates to min() because .min exists in bounds. static_assert(std::numeric_limits::lowest().quantity_from(halfbound_min_origin) == 0.0 * m); // ---- Half-line max-only (halfbound_max_origin: value <= 12000 m) ----------- using qp_hmax = quantity_point; // min() falls back to the representation minimum (lowest); max() returns the bound. static_assert(qp_hmax::min().quantity_from(halfbound_max_origin).numerical_value_in(m) == std::numeric_limits::lowest()); static_assert(qp_hmax::max().quantity_from(halfbound_max_origin) == 12000.0 * m); // numeric_limits mirrors the above. static_assert(std::numeric_limits::min().quantity_from(halfbound_max_origin).numerical_value_in(m) == std::numeric_limits::lowest()); static_assert(std::numeric_limits::max().quantity_from(halfbound_max_origin) == 12000.0 * m); // lowest() has no .min in bounds — falls back to the representation lowest. static_assert(std::numeric_limits::lowest().quantity_from(halfbound_max_origin).numerical_value_in(m) == std::numeric_limits::lowest()); // ============================================================================ // Large-delta tests: deltas much larger than the range width. // // wrap_to_range and reflect_in_range are total functions — defined for all // inputs, not just "one boundary crossing". These tests verify that adding a // delta that covers several full periods still yields the correct result. // (clamp_to_range is excluded: it is idempotent past the boundary — any // value beyond [min, max] produces the same saturated result regardless of // how far it overshoots, so there is nothing new to test for large deltas.) // ============================================================================ // ---- wrap_to_range: range [-180°, 180°), period = 360° --------------------- // 3 full rotations + 45°: same as 45°. static_assert(qp_wrap(3 * 360.0 * deg + 45.0 * deg, wrap_origin).quantity_from(wrap_origin) == 45.0 * deg); // 3 full rotations in the negative direction + 45°: same as 45°. static_assert(qp_wrap(-3 * 360.0 * deg + 45.0 * deg, wrap_origin).quantity_from(wrap_origin) == 45.0 * deg); // 2.5 rotations (= 900°): same as 180° → at upper exclusive boundary → wraps to -180°. static_assert(qp_wrap(2.5 * 360.0 * deg, wrap_origin).quantity_from(wrap_origin) == -180.0 * deg); // operator+=: start at 90°, add 3 full rotations + 30° (= 1110°) → lands at 120°. consteval bool wrap_large_delta_assign() { auto pt = qp_wrap(90.0 * deg, wrap_origin); pt += 3 * 360.0 * deg + 30.0 * deg; return pt.quantity_from(wrap_origin) == 120.0 * deg; } static_assert(wrap_large_delta_assign()); // ---- reflect_in_range: range [-90°, 90°], period = 360° ------------------- // The period of reflect_in_range{[a,b]} is 2*(b−a) = 2*180° = 360°. // After an integer number of periods the point returns to the same position. // Half a period (180°): the "fold-back" point — lands at 0°. static_assert(qp_reflect(180.0 * deg, reflect_origin).quantity_from(reflect_origin) == 0.0 * deg); // 3/4 of a period (270°): bounces off max, then off min → lands at -90°. static_assert(qp_reflect(270.0 * deg, reflect_origin).quantity_from(reflect_origin) == -90.0 * deg); // 3 full periods + 45° (= 1125°): same as 45°. static_assert(qp_reflect(3 * 360.0 * deg + 45.0 * deg, reflect_origin).quantity_from(reflect_origin) == 45.0 * deg); // Negative direction: -(1 full period + 45°) = -405° → same as -45°. static_assert(qp_reflect(-1 * 360.0 * deg - 45.0 * deg, reflect_origin).quantity_from(reflect_origin) == -45.0 * deg); // operator+=: start at 0°, add 1.5 periods (= 540°) → same as adding 180° → 0°. consteval bool reflect_large_delta_assign() { auto pt = qp_reflect(0.0 * deg, reflect_origin); pt += 1.5 * 360.0 * deg; return pt.quantity_from(reflect_origin) == 0.0 * deg; } static_assert(reflect_large_delta_assign()); // ---- wrap_to_range: time-of-day [0 s, 86400 s), period = 86400 s ---------- // 3 full days + 1 hour (= 262800 s): wraps to 3600 s = 01:00. static_assert(time_of_day(3 * 86400.0 * s + 3600.0 * s, midnight).quantity_from(midnight) == 3600.0 * s); // Negative: 3 full days before midnight + 1 hour before end = -3*86400+82800 = -176400 s → 82800 s = 23:00. static_assert(time_of_day(-3 * 86400.0 * s + 82800.0 * s, midnight).quantity_from(midnight) == 82800.0 * s); // operator+=: start at 23:00 (82800 s), add 3 days + 2 hours (= 266400 s) → lands at 01:00 (3600 s). consteval bool time_of_day_multiday_assign() { auto tod = time_of_day(82800.0 * s, midnight); // 23:00 tod += 3 * 86400.0 * s + 2 * 3600.0 * s; // +3 days 2 hours return tod.quantity_from(midnight) == 3600.0 * s; } static_assert(time_of_day_multiday_assign()); // ============================================================================ // Automatic non-negative bounds for natural_point_origin // ============================================================================ // Non-negative base quantities automatically receive check_non_negative bounds // via the natural_origin_base conditional inheritance in quantity_point.h. // isq::length, isq::mass, isq::duration are tagged non_negative → automatic bounds. static_assert(mp_units::detail::HasQuantityBounds>); static_assert(mp_units::detail::HasQuantityBounds>); static_assert(mp_units::detail::HasQuantityBounds>); // isq::angular_measure has no non_negative tag → no automatic bounds. static_assert(!mp_units::detail::HasQuantityBounds>); // The automatic policy type is check_non_negative. static_assert( std::is_same_v::_bounds_)>, mp_units::check_non_negative>); // A non-negative value passes through the policy unchanged. static_assert(mp_units::natural_point_origin_::_bounds_(5.0 * m) == 5.0 * m); static_assert(mp_units::natural_point_origin_::_bounds_(0.0 * m) == 0.0 * m); // ============================================================================ // Non-negative natural_point_origin: bounds inherited by relative origins. // // When QS is tagged non_negative, natural_point_origin receives // check_non_negative bounds automatically. A relative_point_origin with no own // bounds inherits this constraint via enforce_bounds — values may be negative // relative to the offset origin as long as the absolute value stays ≥ 0. // // Example: average_height_origin at +1700 m above the ground floor. // rel = -1500 m → abs = 200 m (≥ 0) → valid, unchanged. // rel = -1700 m → abs = 0 m → valid (boundary). // rel = -1701 m → abs = -1 m (< 0) → check_non_negative fires. // // The violation case cannot be verified at compile time (check_non_negative uses // MP_UNITS_EXPECTS, which may be a no-op in release builds). See the runtime // test for violation coverage with constrained rep. // ============================================================================ QUANTITY_SPEC(avg_height_qs, isq::height); // natural_point_origin gets check_non_negative automatically (isq::height is non_negative). static_assert(mp_units::detail::HasQuantityBounds>); // Relative origin at +1700 m — no own bounds, inherits check_non_negative. inline constexpr struct average_height_origin final : relative_point_origin + 1700.0 * m> { } average_height_origin; // Inherits bounds from ancestor but has none of its own. static_assert(!mp_units::detail::HasQuantityBounds); static_assert(mp_units::detail::any_ancestor_has_bounds(average_height_origin)); using qp_avg_height = quantity_point; // Values that keep absolute height ≥ 0 pass through unchanged. static_assert(qp_avg_height(0.0 * m, average_height_origin).quantity_from(average_height_origin) == 0.0 * m); static_assert(qp_avg_height(500.0 * m, average_height_origin).quantity_from(average_height_origin) == 500.0 * m); static_assert(qp_avg_height(-1500.0 * m, average_height_origin).quantity_from(average_height_origin) == -1500.0 * m); // Boundary: −1700 m relative = 0 m absolute (natural ground level). static_assert(qp_avg_height(-1700.0 * m, average_height_origin).quantity_from(average_height_origin) == -1700.0 * m); } // namespace