diff --git a/Makefile b/Makefile index 0db231e..cca822d 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ Obj=bin/errors.o \ bin/location.o \ bin/parser.o \ bin/unicode.o \ + bin/number.o \ bin/unicode_tables.o all: bin/musique bin/unit-tests diff --git a/src/musique.hh b/src/musique.hh index 9a5f51f..96f91cd 100644 --- a/src/musique.hh +++ b/src/musique.hh @@ -340,6 +340,32 @@ struct Parser Result ensure(Token::Type type) const; }; +// Number type supporting integer and fractional constants +// Invariant: gcd(num, den) == 1, after any operation +struct Number +{ + i64 num = 0, den = 1; + + auto as_int() const -> i64; // Returns self as int + auto simplify() const -> Number; // Returns self, but with gcd(num, den) == 1 + void simplify_inplace(); // Update self, to have gcd(num, den) == 1 + + bool operator==(Number const&) const; + bool operator!=(Number const&) const; + std::strong_ordering operator<=>(Number const&) const; + + Number operator+(Number const& rhs) const; + Number& operator+=(Number const& rhs); + Number operator-(Number const& rhs) const; + Number& operator-=(Number const& rhs); + Number operator*(Number const& rhs) const; + Number& operator*=(Number const& rhs); + Number operator/(Number const& rhs) const; + Number& operator/=(Number const& rhs); +}; + +std::ostream& operator<<(std::ostream& os, Number const& num); + namespace errors { Error unrecognized_character(u32 invalid_character); diff --git a/src/number.cc b/src/number.cc new file mode 100644 index 0000000..018ef16 --- /dev/null +++ b/src/number.cc @@ -0,0 +1,116 @@ +#include +#include +#include + +auto Number::as_int() const -> i64 +{ + // We don't perform GCD simplification in place due to constness + auto self = simplify(); + assert(self.den == 1 || self.num == 0, "Implicit coarce to integer while holding fractional number is not allowed"); + return self.num; +} + +auto Number::simplify() const -> Number +{ + auto copy = *this; + copy.simplify_inplace(); + return copy; +} + +// TODO This function may behave weirdly for maximum & minimum values of i64 +void Number::simplify_inplace() +{ + for (;;) { + if (auto d = std::gcd(num, den); d != 1) { + num /= d; + den /= d; + } else { + break; + } + } + + if (den < 0) { + // When num < 0 yields num > 0 + // When num > 0 yields num < 0 + den = -den; + num = -num; + } +} + +bool Number::operator==(Number const& rhs) const +{ + return (*this <=> rhs) == 0; +} + +bool Number::operator!=(Number const& rhs) const +{ + return !(*this == rhs); +} + +std::strong_ordering Number::operator<=>(Number const& rhs) const +{ + return (num * rhs.den - den * rhs.num) <=> 0; +} + +Number Number::operator+(Number const& rhs) const +{ + int dens_lcm = std::lcm(den, rhs.den); + return Number{(num * (dens_lcm / den)) + (rhs.num * (dens_lcm / rhs.den)), dens_lcm}.simplify(); +} + +Number& Number::operator+=(Number const& rhs) +{ + int dens_lcm = std::lcm(den, rhs.den); + num = (num * (dens_lcm / den)) + (rhs.num * (dens_lcm / rhs.den)); + den = dens_lcm; + simplify_inplace(); + return *this; +} + +Number Number::operator-(Number const& rhs) const +{ + int dens_lcm = std::lcm(den, rhs.den); + return Number{(num * (dens_lcm / den)) - (rhs.num * (dens_lcm / rhs.den)), dens_lcm}.simplify(); +} + +Number& Number::operator-=(Number const& rhs) +{ + int dens_lcm = std::lcm(den, rhs.den); + num = (num * (dens_lcm / den)) - (rhs.num * (dens_lcm / rhs.den)); + den = dens_lcm; + simplify_inplace(); + return *this; +} + +Number Number::operator*(Number const& rhs) const +{ + return Number{num * rhs.num, den * rhs.den}.simplify(); +} + +Number& Number::operator*=(Number const& rhs) +{ + num *= rhs.num; + den *= rhs.den; + simplify_inplace(); + return *this; +} + +Number Number::operator/(Number const& rhs) const +{ + return Number{num * rhs.den, den * rhs.num}.simplify(); +} + +Number& Number::operator/=(Number const& rhs) +{ + num *= rhs.den; + den *= rhs.num; + simplify_inplace(); + return *this; +} + +std::ostream& operator<<(std::ostream& os, Number const& n) +{ + return n.den == 1 + ? os << n.num + : os << n.num << '/' << n.den; +} diff --git a/src/tests/number.cc b/src/tests/number.cc new file mode 100644 index 0000000..c58b1f6 --- /dev/null +++ b/src/tests/number.cc @@ -0,0 +1,35 @@ +#include +#include + +using namespace boost::ut; +using namespace std::string_view_literals; + +suite number_test = [] { + "Number"_test = [] { + should("provide arithmetic operators") = [] { + expect(eq(Number{1, 8} + Number{3, 4}, Number{ 7, 8})) << "for expr: (1/8) + (3/4)"; + expect(eq(Number{1, 8} - Number{3, 4}, Number{-5, 8})) << "for expr: (1/8) - (3/4)"; + expect(eq(Number{1, 8} * Number{3, 4}, Number{ 3, 32})) << "for expr: (1/8) * (3/4)"; + expect(eq(Number{1, 8} / Number{3, 4}, Number{ 1, 6})) << "for expr: (1/8) / (3/4)"; + }; + + should("provide assignment operators") = [] { + Number n; + n={1,8}; n += {3,4}; expect(eq(n, Number{ 7, 8})) << "for expr: (1/8) += (3/4)"; + n={1,8}; n -= {3,4}; expect(eq(n, Number{-5, 8})) << "for expr: (1/8) -= (3/4)"; + n={1,8}; n *= {3,4}; expect(eq(n, Number{ 3, 32})) << "for expr: (1/8) *= (3/4)"; + n={1,8}; n /= {3,4}; expect(eq(n, Number{ 1, 6})) << "for expr: (1/8) /= (3/4)"; + }; + + should("be comperable") = [] { + expect(gt(Number{1, 4}, Number{1, 8})); + expect(eq(Number{2, 4}, Number{1, 2})); + }; + + should("be convertable to int") = [] { + expect(eq(Number{4, 2}.as_int(), 2)) << "for fraction 4/2"; + expect(eq(Number{2, 1}.as_int(), 2)) << "for fraction 2/1"; + expect(eq(Number{0, 1000}.as_int(), 0)) << "for fraction 0/1000"; + }; + }; +};