@@ -121,40 +121,99 @@ template<QuantityPoint ToQP, typename FwdFromQP, QuantityPoint FromQP = std::rem
121121[[nodiscard]] constexpr QuantityPoint auto sudo_cast (FwdFromQP&& qp)
122122{
123123 if constexpr (is_same_v<MP_UNITS_NONCONST_TYPE (ToQP::point_origin), MP_UNITS_NONCONST_TYPE (FromQP::point_origin)>) {
124+ // Same origin: delegate entirely to the quantity sudo_cast — no offset arithmetic needed.
124125 return quantity_point{
125126 sudo_cast<typename ToQP::quantity_type>(std::forward<FwdFromQP>(qp).quantity_from (FromQP::point_origin)),
126- FromQP ::point_origin};
127+ ToQP ::point_origin};
127128 } else {
128- // it's unclear how hard we should try to avoid truncation here. For now, the only corner case we cater for,
129- // is when the range of the quantity type of at most one of QP or ToQP doesn't cover the offset between the
130- // point origins. In that case, we need to be careful to ensure we use the quantity type with the larger range
131- // of the two to perform the point_origin conversion.
129+ // Different origins: we need to (a) convert the rep/unit, (b) add the origin offset.
130+ // The order and intermediate unit choice matters for accuracy and overflow avoidance.
131+ //
132+ // Strategy: pick an intermediate unit/rep, then compute:
133+ // result = sudo_cast<intermediate>(input_quantity) + offset
134+ // where offset is the static difference between the two origins expressed in the
135+ // intermediate unit. Finally, sudo_cast the sum to the target type.
136+ //
132137 // Numerically, we'll potentially need to do three things:
133138 // (a) cast the representation type
134139 // (b) scale the numerical value
135140 // (c) add/subtract the origin difference
136- // In the following, we carefully select the order of these three operations: each of (a) and (b) is scheduled
137- // either before or after (c), such that (c) acts on the largest range possible among all combination of source
138- // and target unit and representation.
141+ // The intermediate unit determines the order of (b) and (c).
142+
139143 constexpr UnitMagnitude auto c_mag =
140144 mp_units::get_canonical_unit (FromQP::unit).mag / mp_units::get_canonical_unit (ToQP::unit).mag ;
141145 using type_traits = conversion_type_traits<c_mag, typename FromQP::rep, typename ToQP::rep>;
146+ using c_rep_type = type_traits::c_rep_type;
142147 using c_type = type_traits::c_type;
143- if constexpr (get_value<long double >(c_mag) > 1 .) {
144- // original unit had a larger unit magnitude; if we first convert to the common representation but retain the
145- // unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal
146- // for the offset computation.
147- return sudo_cast<ToQP>(
148- sudo_cast<quantity_point<FromQP::reference, FromQP::point_origin, c_type>>(std::forward<FwdFromQP>(qp))
149- .point_for (ToQP::point_origin));
148+
149+ // Helper: statically compute the origin offset expressed in the given quantity type Q.
150+ // We go via quantity_point subtraction to handle all origin relationships correctly,
151+ // including cases where two zeroth_point_origins of different units exist.
152+ constexpr auto offset_as = [&]<typename Q>(std::type_identity<Q>) {
153+ constexpr auto zero = typename Q::rep{0 } * Q::reference;
154+ return sudo_cast<Q>(quantity_point{zero, FromQP::point_origin} - quantity_point{zero, ToQP::point_origin});
155+ };
156+
157+ constexpr auto output_unit_ref = make_reference (FromQP::quantity_spec, ToQP::unit);
158+
159+ if constexpr (equivalent (FromQP::unit, ToQP::unit)) {
160+ // Same unit, different origin: no scaling needed — add the offset directly in the
161+ // common rep without any unit conversion. This is always exact.
162+ using intermediate_type = quantity<FromQP::reference, c_rep_type>;
163+ constexpr auto offset = offset_as (std::type_identity<intermediate_type>{});
164+ return quantity_point{
165+ sudo_cast<typename ToQP::quantity_type>(
166+ sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from (FromQP::point_origin)) + offset),
167+ ToQP::point_origin};
168+ } else if constexpr (treat_as_floating_point<c_type>) {
169+ // Floating-point intermediate: prefer the larger unit to minimise the magnitude of scaling
170+ // applied to the input value, then use point_for to let the library's common-unit quantity
171+ // arithmetic handle the origin offset (e.g. 2.0 km + 42 m → 2042.0 m, not 2.0 + 0.042 km).
172+ // This avoids the FP precision loss that would occur if we expressed the offset explicitly
173+ // in the intermediate unit.
174+ constexpr auto intermediate_ref = [&]() {
175+ if constexpr (!is_integral (pow<-1 >(c_mag)))
176+ return FromQP::reference; // from-unit is larger
177+ else
178+ return output_unit_ref; // to-unit is larger (or equal)
179+ }();
180+ using intermediate_type = quantity<intermediate_ref, c_type>;
181+ return quantity_point{
182+ sudo_cast<typename ToQP::quantity_type>(
183+ quantity_point{sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from (FromQP::point_origin)),
184+ FromQP::point_origin}
185+ .point_for (ToQP::point_origin)
186+ .quantity_from (ToQP::point_origin)),
187+ ToQP::point_origin};
150188 } else {
151- // new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing
152- // truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two
153- // representation types. Then, we can perform the offset computation.
154- return sudo_cast<ToQP>(
155- sudo_cast<quantity_point<make_reference (FromQP::quantity_spec, ToQP::unit), FromQP::point_origin, c_type>>(
156- std::forward<FwdFromQP>(qp))
157- .point_for (ToQP::point_origin));
189+ // Integer intermediate: use the output unit so the result is already in the right
190+ // unit before add, giving the most-accurate truncation. However, if the offset
191+ // would overflow in the output unit (e.g. because it was defined in a larger one),
192+ // fall back to the input unit.
193+ //
194+ // Detect overflow by computing the offset in long double expressed in the output unit
195+ // and comparing against the representable range of c_rep_type.
196+ constexpr long double offset_in_output_ld =
197+ offset_as (std::type_identity<quantity<output_unit_ref, long double >>{}).numerical_value_in (ToQP::unit);
198+ constexpr bool offset_fits_in_output =
199+ offset_in_output_ld >= static_cast <long double >(std::numeric_limits<c_rep_type>::min ()) &&
200+ offset_in_output_ld <= static_cast <long double >(std::numeric_limits<c_rep_type>::max ());
201+ if constexpr (offset_fits_in_output) {
202+ using intermediate_type = quantity<output_unit_ref, c_rep_type>;
203+ constexpr auto offset = offset_as (std::type_identity<intermediate_type>{});
204+ return quantity_point{
205+ sudo_cast<typename ToQP::quantity_type>(
206+ sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from (FromQP::point_origin)) + offset),
207+ ToQP::point_origin};
208+ } else {
209+ // offset overflows in the output unit — use the input unit instead
210+ using intermediate_type = quantity<FromQP::reference, c_rep_type>;
211+ constexpr auto offset = offset_as (std::type_identity<intermediate_type>{});
212+ return quantity_point{
213+ sudo_cast<typename ToQP::quantity_type>(
214+ sudo_cast<intermediate_type>(std::forward<FwdFromQP>(qp).quantity_from (FromQP::point_origin)) + offset),
215+ ToQP::point_origin};
216+ }
158217 }
159218 }
160219}
0 commit comments