Skip to content

Commit 2960d31

Browse files
committed
feat: negative constants support added
Resolves #510
1 parent 8c9bc1b commit 2960d31

File tree

6 files changed

+796
-649
lines changed

6 files changed

+796
-649
lines changed

src/core/include/mp-units/bits/unit_magnitude.h

Lines changed: 207 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,24 @@ import std;
5252
namespace mp_units::detail {
5353

5454
template<typename T>
55-
concept MagArg = std::integral<T> || is_mag_constant<T>;
55+
concept MagArg = std::integral<T> || is_mag_constant<T> || is_same_v<T, ratio>;
56+
57+
/**
58+
* @brief A sentinel type representing the factor (-1) in a unit magnitude.
59+
*
60+
* Always appears as the first element in a pack when present. Two occurrences cancel each other out.
61+
* This enables support for negative magnitudes in named constants.
62+
*/
63+
struct negative_tag {};
64+
65+
template<auto H, auto...>
66+
consteval auto first_mag_arg()
67+
{
68+
return H;
69+
}
70+
71+
template<typename M>
72+
constexpr bool is_negative_tag = is_same_v<MP_UNITS_REMOVE_CONST(M), negative_tag>;
5673

5774
/**
5875
* @brief Any type which can be used as a basis vector in a power_v.
@@ -91,10 +108,18 @@ template<typename T>
91108
return get_base_value(T::base);
92109
else if constexpr (is_mag_constant<T>)
93110
return element._value_;
111+
else if constexpr (is_negative_tag<T>)
112+
return std::intmax_t{-1}; // sorts before all positive prime bases
94113
else
95114
return element;
96115
}
97116

117+
template<MagArg auto V>
118+
constexpr bool is_nonzero_mag_arg = get_base_value(V) != 0;
119+
120+
template<MagArg auto V>
121+
constexpr bool is_positive_mag_arg = get_base_value(V) > 0;
122+
98123
template<auto V, ratio R>
99124
[[nodiscard]] consteval auto power_v_or_T()
100125
{
@@ -129,6 +154,13 @@ template<typename T>
129154
//
130155
// Note that since this function should only be called at compile time, the point of these
131156
// terminations is to act as "static_assert substitutes", not to actually terminate at runtime.
157+
158+
// The negative_tag sentinel represents the factor (-1).
159+
if constexpr (is_negative_tag<decltype(get_base(el))>) {
160+
static_assert(!std::is_unsigned_v<T>, "Cannot represent a negative magnitude value in an unsigned type");
161+
return widen_t<T>{-1};
162+
}
163+
132164
const auto exp = get_exponent(el);
133165

134166
if (exp.num < 0) {
@@ -156,18 +188,28 @@ template<typename T>
156188

157189
[[nodiscard]] consteval bool is_rational_impl(auto element)
158190
{
159-
return std::is_integral_v<decltype(get_base(element))> && get_exponent(element).den == 1;
191+
if constexpr (is_negative_tag<decltype(element)>)
192+
return true; // (-1) is a rational number
193+
else
194+
return std::is_integral_v<decltype(get_base(element))> && get_exponent(element).den == 1;
160195
}
161196

162197
[[nodiscard]] consteval bool is_integral_impl(auto element)
163198
{
164-
return is_rational_impl(element) && get_exponent(element).num > 0;
199+
if constexpr (is_negative_tag<decltype(element)>)
200+
return true; // (-1) is an integer
201+
else
202+
return is_rational_impl(element) && get_exponent(element).num > 0;
165203
}
166204

167205
[[nodiscard]] consteval bool is_positive_integral_power_impl(auto element)
168206
{
169-
auto exp = get_exponent(element);
170-
return exp.den == 1 && exp.num > 0;
207+
if constexpr (is_negative_tag<decltype(element)>)
208+
return false; // (-1) is not a positive factor
209+
else {
210+
auto exp = get_exponent(element);
211+
return exp.den == 1 && exp.num > 0;
212+
}
171213
}
172214

173215
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -195,9 +237,19 @@ template<auto M>
195237
[[nodiscard]] consteval auto only_negative_mag_constants(unit_magnitude<M> m);
196238

197239
template<MagArg auto Base, int Num, int Den = 1>
198-
requires(get_base_value(Base) > 0)
240+
requires is_positive_mag_arg<Base>
199241
[[nodiscard]] consteval UnitMagnitude auto mag_power_lazy();
200242

243+
// Forward declarations; fully defined after unit_magnitude
244+
template<auto H, auto... Rest>
245+
[[nodiscard]] consteval auto abs_magnitude(unit_magnitude<H, Rest...>);
246+
[[nodiscard]] consteval auto abs_magnitude(unit_magnitude<>);
247+
248+
template<int Num, int Den>
249+
[[nodiscard]] consteval auto pow_magnitude(unit_magnitude<>);
250+
template<int Num, int Den, auto H, auto... Rest>
251+
[[nodiscard]] consteval auto pow_magnitude(unit_magnitude<H, Rest...>);
252+
201253
template<typename T>
202254
struct magnitude_base {};
203255

@@ -218,7 +270,10 @@ struct magnitude_base<unit_magnitude<H, T...>> {
218270
} else {
219271
if constexpr (is_same_v<decltype(get_base(H)), decltype(get_base(H2))>) {
220272
constexpr auto partial_product = unit_magnitude<T...>{} * unit_magnitude<T2...>{};
221-
if constexpr (get_exponent(H) + get_exponent(H2) == 0) {
273+
if constexpr (is_negative_tag<decltype(get_base(H))>) {
274+
// (-1) * (-1) = 1: two negatives cancel each other out
275+
return partial_product;
276+
} else if constexpr (get_exponent(H) + get_exponent(H2) == 0) {
222277
return partial_product;
223278
} else {
224279
// Make a new power_v with the common base of H and H2, whose power is their powers' sum.
@@ -436,7 +491,7 @@ struct unit_magnitude : magnitude_base<unit_magnitude<Ms...>> {
436491
if constexpr (Num == 0) {
437492
return unit_magnitude<>{};
438493
} else {
439-
return unit_magnitude<power_v_or_T<get_base(Ms), get_exponent(Ms) * ratio{Num, Den}>()...>{};
494+
return pow_magnitude<Num, Den>(unit_magnitude{});
440495
}
441496
}
442497

@@ -447,7 +502,10 @@ struct unit_magnitude : magnitude_base<unit_magnitude<Ms...>> {
447502
return (mp_units::detail::integer_part(unit_magnitude<Ms>{}) * ... * unit_magnitude<>{});
448503
}
449504

450-
[[nodiscard]] friend consteval auto denominator(unit_magnitude) { return numerator(pow<-1>(unit_magnitude{})); }
505+
[[nodiscard]] friend consteval auto denominator(unit_magnitude)
506+
{
507+
return numerator(pow<-1>(mp_units::detail::abs_magnitude(unit_magnitude{})));
508+
}
451509

452510
[[nodiscard]] friend consteval auto remove_positive_powers(unit_magnitude)
453511
{
@@ -476,7 +534,13 @@ struct unit_magnitude : magnitude_base<unit_magnitude<Ms...>> {
476534
template<typename T>
477535
[[nodiscard]] friend consteval ratio get_power([[maybe_unused]] T base, unit_magnitude)
478536
{
479-
return ((get_base_value(Ms) == base ? get_exponent(Ms) : ratio{0}) + ... + ratio{0});
537+
[[maybe_unused]] auto is_base = [&](auto element) consteval {
538+
if constexpr (is_negative_tag<decltype(element)>)
539+
return false; // (-1) is not a base, it's a special sentinel
540+
else
541+
return get_base_value(element) == base;
542+
};
543+
return ((is_base(Ms) ? get_exponent(Ms) : ratio{0}) + ... + ratio{0});
480544
}
481545

482546
[[nodiscard]] friend consteval std::intmax_t extract_power_of_10(unit_magnitude mag)
@@ -492,36 +556,71 @@ struct unit_magnitude : magnitude_base<unit_magnitude<Ms...>> {
492556
template<typename CharT, std::output_iterator<CharT> Out>
493557
[[nodiscard]] friend constexpr Out magnitude_symbol(Out out, unit_magnitude, const unit_symbol_formatting& fmt)
494558
{
495-
if constexpr (unit_magnitude{} == unit_magnitude<1>{}) {
559+
if constexpr (sizeof...(Ms) == 0) {
496560
return out;
497561
} else {
498-
constexpr auto extract_res = extract_components(unit_magnitude{});
499-
constexpr UnitMagnitude auto ratio = std::get<0>(extract_res);
500-
constexpr UnitMagnitude auto num_constants = std::get<1>(extract_res);
501-
constexpr UnitMagnitude auto den_constants = std::get<2>(extract_res);
502-
constexpr std::intmax_t exp10 = extract_power_of_10(ratio);
503-
if constexpr (abs(exp10) < 3) {
504-
// print the value as a regular number (without exponent)
505-
constexpr UnitMagnitude auto num = numerator(unit_magnitude{});
506-
constexpr UnitMagnitude auto den = denominator(unit_magnitude{});
507-
// TODO address the below
508-
static_assert(ratio == num / den, "Printing rational powers not yet supported");
509-
return magnitude_symbol_impl<CharT, num, den, num_constants, den_constants, 0>(out, fmt);
562+
// If the magnitude is negative (starts with negative_tag), prepend '-' and delegate.
563+
constexpr bool is_negative = sizeof...(Ms) > 0 && is_negative_tag<decltype(first_mag_arg<Ms...>())>;
564+
if constexpr (is_negative) {
565+
*out++ = '-';
566+
constexpr UnitMagnitude auto abs_mag = mp_units::detail::abs_magnitude(unit_magnitude{});
567+
return magnitude_symbol<CharT>(out, abs_mag, fmt);
510568
} else {
511-
// print the value as a number with exponent
512-
// if user wanted a regular number for this magnitude then probably a better scaled unit should be used
513-
constexpr UnitMagnitude auto base = ratio / mag_power_lazy<10, exp10>();
514-
constexpr UnitMagnitude auto num = numerator(base);
515-
constexpr UnitMagnitude auto den = denominator(base);
516-
517-
// TODO address the below
518-
static_assert(base == num / den, "Printing rational powers not yet supported");
519-
return magnitude_symbol_impl<CharT, num, den, num_constants, den_constants, exp10>(out, fmt);
569+
constexpr auto extract_res = extract_components(unit_magnitude{});
570+
constexpr UnitMagnitude auto ratio = std::get<0>(extract_res);
571+
constexpr UnitMagnitude auto num_constants = std::get<1>(extract_res);
572+
constexpr UnitMagnitude auto den_constants = std::get<2>(extract_res);
573+
constexpr std::intmax_t exp10 = extract_power_of_10(ratio);
574+
if constexpr (detail::abs(exp10) < 3) {
575+
// print the value as a regular number (without exponent)
576+
constexpr UnitMagnitude auto num = numerator(unit_magnitude{});
577+
constexpr UnitMagnitude auto den = denominator(unit_magnitude{});
578+
// TODO address the below
579+
static_assert(ratio == num / den, "Printing rational powers not yet supported");
580+
return magnitude_symbol_impl<CharT, num, den, num_constants, den_constants, 0>(out, fmt);
581+
} else {
582+
// print the value as a number with exponent
583+
// if user wanted a regular number for this magnitude then probably a better scaled unit should be used
584+
constexpr UnitMagnitude auto base = ratio / mp_units::detail::mag_power_lazy<10, exp10>();
585+
constexpr UnitMagnitude auto num = numerator(base);
586+
constexpr UnitMagnitude auto den = denominator(base);
587+
588+
// TODO address the below
589+
static_assert(base == num / den, "Printing rational powers not yet supported");
590+
return magnitude_symbol_impl<CharT, num, den, num_constants, den_constants, exp10>(out, fmt);
591+
}
520592
}
521593
}
522594
}
523595
};
524596

597+
template<int Num, int Den>
598+
[[nodiscard]] consteval auto pow_magnitude(unit_magnitude<>)
599+
{
600+
return unit_magnitude<>{};
601+
}
602+
603+
template<int Num, int Den, auto H, auto... Rest>
604+
[[nodiscard]] consteval auto pow_magnitude(unit_magnitude<H, Rest...>)
605+
{
606+
if constexpr (is_negative_tag<decltype(H)>) {
607+
// Raising (-1) to the power Num/Den (in lowest terms):
608+
// - even denominator → taking an even root of a negative number → hard error
609+
// - even numerator → result is positive (+1), negative_tag cancels out
610+
// - odd numerator → result is negative (-1), negative_tag is preserved
611+
static_assert(ratio{Num, Den}.den % 2 == 1, "Cannot take even root of negative magnitude");
612+
constexpr auto rest_powered = pow_magnitude<Num, Den>(unit_magnitude<Rest...>{});
613+
if constexpr (ratio{Num, Den}.num % 2 == 0)
614+
return rest_powered;
615+
else
616+
return unit_magnitude<H>{} * rest_powered;
617+
} else {
618+
// No negative_tag at front: apply power uniformly to all elements.
619+
return unit_magnitude<power_v_or_T<get_base(H), get_exponent(H) * ratio{Num, Den}>(),
620+
power_v_or_T<get_base(Rest), get_exponent(Rest) * ratio{Num, Den}>()...>{};
621+
}
622+
}
623+
525624
[[nodiscard]] consteval auto common_magnitude(unit_magnitude<>, UnitMagnitude auto m)
526625
{
527626
return remove_positive_powers(m);
@@ -532,26 +631,64 @@ struct unit_magnitude : magnitude_base<unit_magnitude<Ms...>> {
532631
}
533632
[[nodiscard]] consteval auto common_magnitude(unit_magnitude<> m, unit_magnitude<>) { return m; }
534633

634+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
635+
// `abs_magnitude` — strips the leading negative_tag if present, leaving the absolute value.
636+
637+
template<auto H, auto... Rest>
638+
[[nodiscard]] consteval auto abs_magnitude(unit_magnitude<H, Rest...>)
639+
{
640+
if constexpr (is_negative_tag<decltype(H)>)
641+
return unit_magnitude<Rest...>{};
642+
else
643+
return unit_magnitude<H, Rest...>{};
644+
}
645+
646+
[[nodiscard]] consteval auto abs_magnitude(unit_magnitude<>) { return unit_magnitude<>{}; }
647+
648+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
649+
// `magnitude_is_positive` — checks whether a magnitude is positive (i.e., has no negative_tag).
650+
651+
// Uses a consteval function rather than `is_same_v<decltype(M), decltype(abs_magnitude(M))>` because GCC 12
652+
// incorrectly evaluates the latter to `false` for genuinely positive magnitudes in some constexpr contexts.
653+
[[nodiscard]] consteval bool check_magnitude_is_positive(unit_magnitude<>) { return true; }
654+
655+
template<auto H, auto... Rest>
656+
[[nodiscard]] consteval bool check_magnitude_is_positive(unit_magnitude<H, Rest...>)
657+
{
658+
return !is_negative_tag<decltype(H)>;
659+
}
660+
661+
template<UnitMagnitude auto M>
662+
constexpr bool magnitude_is_positive = check_magnitude_is_positive(M);
535663

536664
// The largest integer which can be extracted from any magnitude with only a single basis vector.
537665
template<auto M>
538666
[[nodiscard]] consteval auto integer_part(unit_magnitude<M>)
539667
{
540-
constexpr auto power_num = get_exponent(M).num;
541-
constexpr auto power_den = get_exponent(M).den;
542-
543-
if constexpr (std::is_integral_v<decltype(get_base(M))> && (power_num >= power_den)) {
544-
// largest integer power
545-
return unit_magnitude<power_v_or_T<get_base(M), power_num / power_den>()>{}; // Note: integer division intended
668+
// The negative_tag is the integer (-1): include it in the integer part.
669+
// The else is required so that the rest of the body (which calls get_exponent/get_base on M)
670+
// is not instantiated for negative_tag, avoiding conflicting return type deductions.
671+
if constexpr (is_negative_tag<decltype(M)>) {
672+
return unit_magnitude<M>{};
546673
} else {
547-
return unit_magnitude<>{};
674+
constexpr auto power_num = get_exponent(M).num;
675+
constexpr auto power_den = get_exponent(M).den;
676+
677+
if constexpr (std::is_integral_v<decltype(get_base(M))> && (power_num >= power_den)) {
678+
// largest integer power
679+
return unit_magnitude<power_v_or_T<get_base(M), power_num / power_den>()>{}; // Note: integer division intended
680+
} else {
681+
return unit_magnitude<>{};
682+
}
548683
}
549684
}
550685

551686
template<auto M>
552687
[[nodiscard]] consteval auto remove_positive_power(unit_magnitude<M> m)
553688
{
554-
if constexpr (get_exponent(M).num < 0) {
689+
if constexpr (is_negative_tag<decltype(M)>)
690+
return unit_magnitude<>{}; // negative_tag is a sign sentinel, not a basis element; exclude it
691+
else if constexpr (get_exponent(M).num < 0) {
555692
return m;
556693
} else {
557694
return unit_magnitude<>{};
@@ -561,7 +698,9 @@ template<auto M>
561698
template<auto M>
562699
[[nodiscard]] consteval auto remove_mag_constants(unit_magnitude<M> m)
563700
{
564-
if constexpr (is_mag_constant<decltype(get_base(M))>)
701+
if constexpr (is_negative_tag<decltype(M)>)
702+
return m; // negative_tag is a sign marker, not a mag_constant; keep it in the ratio part
703+
else if constexpr (is_mag_constant<decltype(get_base(M))>)
565704
return unit_magnitude<>{};
566705
else
567706
return m;
@@ -570,7 +709,9 @@ template<auto M>
570709
template<auto M>
571710
[[nodiscard]] consteval auto only_positive_mag_constants(unit_magnitude<M> m)
572711
{
573-
if constexpr (is_mag_constant<decltype(get_base(M))> && get_exponent(M) >= 0)
712+
if constexpr (is_negative_tag<decltype(M)>)
713+
return unit_magnitude<>{};
714+
else if constexpr (is_mag_constant<decltype(get_base(M))> && get_exponent(M) >= 0)
574715
return m;
575716
else
576717
return unit_magnitude<>{};
@@ -579,7 +720,9 @@ template<auto M>
579720
template<auto M>
580721
[[nodiscard]] consteval auto only_negative_mag_constants(unit_magnitude<M> m)
581722
{
582-
if constexpr (is_mag_constant<decltype(get_base(M))> && get_exponent(M) < 0)
723+
if constexpr (is_negative_tag<decltype(M)>)
724+
return unit_magnitude<>{};
725+
else if constexpr (is_mag_constant<decltype(get_base(M))> && get_exponent(M) < 0)
583726
return m;
584727
else
585728
return unit_magnitude<>{};
@@ -621,9 +764,28 @@ constexpr auto prime_factorization_v = prime_factorization<N>::value;
621764
template<MagArg auto V>
622765
[[nodiscard]] consteval UnitMagnitude auto make_magnitude()
623766
{
624-
if constexpr (is_mag_constant<MP_UNITS_REMOVE_CONST(decltype(V))>)
767+
using mag_arg_type = MP_UNITS_REMOVE_CONST(decltype(V));
768+
769+
if constexpr (is_mag_constant<mag_arg_type>)
625770
return unit_magnitude<V>{};
626-
else
771+
else if constexpr (is_same_v<mag_arg_type, ratio>) {
772+
// ratio{num, den}: factor out the sign, then factorize num and den
773+
constexpr ratio abs_v{V.num < 0 ? -V.num : V.num, V.den};
774+
constexpr bool negative = V.num < 0;
775+
constexpr UnitMagnitude auto abs_mag = prime_factorization_v<abs_v.num> / prime_factorization_v<abs_v.den>;
776+
if constexpr (negative)
777+
return unit_magnitude<negative_tag{}>{} * abs_mag;
778+
else
779+
return abs_mag;
780+
} else if constexpr (std::integral<mag_arg_type>) {
781+
// Integer NTTP: factor out the sign, then prime-factorize the absolute value
782+
constexpr auto abs_v = V < 0 ? -V : V;
783+
constexpr UnitMagnitude auto abs_mag = prime_factorization_v<abs_v>;
784+
if constexpr (V < 0)
785+
return unit_magnitude<negative_tag{}>{} * abs_mag;
786+
else
787+
return abs_mag;
788+
} else
627789
return prime_factorization_v<V>;
628790
}
629791

0 commit comments

Comments
 (0)