Initial BigInt implementation #1

Merged
me merged 2 commits from bigint into main 2025-09-08 21:31:52 -06:00
6 changed files with 834 additions and 2 deletions

View File

@ -11,6 +11,39 @@ project(
find_package(GTest CONFIG REQUIRED)
file(GLOB LIB_SRCS "common/**.cpp")
add_library(
common STATIC
${LIB_SRCS}
)
target_include_directories(
common PUBLIC
common/
)
file(GLOB LIB_TESTS "common-tests/src/**.cpp")
foreach(FILE IN LISTS LIB_TESTS)
cmake_path(GET FILE STEM TEST)
add_executable("common-tests-${TEST}" "${FILE}")
target_link_libraries(
"common-tests-${TEST}" PRIVATE
common
GTest::gtest_main
)
set_target_properties(
"common-tests-${TEST}" PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/common-tests/bin"
RUNTIME_OUTPUT_NAME "test-${TEST}"
SUFFIX ""
)
endforeach()
file(GLOB SOURCES "problem-*/")
foreach(FILE IN LISTS SOURCES)
@ -37,24 +70,24 @@ foreach(FILE IN LISTS SOURCES)
target_link_libraries(
"${PROBLEM}" PRIVATE
common
)
target_link_libraries(
"test-${PROBLEM}" PRIVATE
common
GTest::gtest_main
)
target_include_directories(
"${PROBLEM}" PRIVATE
"${FILE}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/common"
)
target_include_directories(
"test-${PROBLEM}" PRIVATE
"${FILE}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/common"
)
set_target_properties(

229
common-tests/src/bigint.cpp Normal file
View File

@ -0,0 +1,229 @@
/*
* Project Euler Solutions - Test File
* ------------------------------------
* Problem : Testing
* URL : https://projecteuler.net/problem=test
*
* Author : erick-alcachofa
* Created : Friday, September 05 2025
*
* Notes:
* -
*
* License : GNU Affero General Public License v3.0 (AGPLv3)
* https://www.gnu.org/licenses/agpl-3.0.html
*/
#include <gtest/gtest.h>
#include "bigint.hpp"
// Helper macro for creating BigInt from string, fails test on error
#define BI(str) BigInt::fromString(str).value()
TEST(FromString, HandlesPositiveNumbers) {
EXPECT_EQ(BI("12345").toString(), "12345");
EXPECT_EQ(BI("0").toString(), "0");
EXPECT_EQ(BI("99").toString(), "99");
EXPECT_EQ(BI("100").toString(), "100");
}
TEST(FromString, HandlesNegativeNumbers) {
EXPECT_EQ(BI("-12345").toString(), "-12345");
EXPECT_EQ(BI("-99").toString(), "-99");
}
TEST(FromString, HandlesZero) {
EXPECT_EQ(BI("0").toString(), "0");
EXPECT_EQ(BI("-0").toString(), "0"); // Canonical zero
}
TEST(FromString, HandlesWhitespaceAndSigns) {
EXPECT_EQ(BI(" 123 ").toString(), "123");
EXPECT_EQ(BI("+456").toString(), "456");
EXPECT_EQ(BI(" -789 ").toString(), "-789");
}
TEST(FromString, HandlesInvalidInput) {
EXPECT_FALSE(BigInt::fromString("").has_value());
EXPECT_FALSE(BigInt::fromString("-").has_value());
EXPECT_FALSE(BigInt::fromString("12a34").has_value());
EXPECT_FALSE(BigInt::fromString("abc").has_value());
}
TEST(Addition, PositivePlusPositive) {
EXPECT_EQ((BI("123") + BI("456")).toString(), "579");
EXPECT_EQ((BI("99") + BI("1")).toString(), "100"); // With carry
EXPECT_EQ((BI("9999") + BI("1")).toString(), "10000");
}
TEST(Addition, NegativePlusNegative) {
EXPECT_EQ((BI("-123") + BI("-456")).toString(), "-579");
EXPECT_EQ((BI("-99") + BI("-1")).toString(), "-100");
}
TEST(Addition, MixedSigns) {
EXPECT_EQ((BI("500") + BI("-200")).toString(), "300");
EXPECT_EQ((BI("200") + BI("-500")).toString(), "-300");
EXPECT_EQ((BI("-500") + BI("200")).toString(), "-300");
EXPECT_EQ((BI("-200") + BI("500")).toString(), "300");
EXPECT_EQ((BI("200") + BI("-200")).toString(), "0");
}
TEST(Subtraction, PositiveMinusPositive) {
EXPECT_EQ((BI("500") - BI("200")).toString(), "300");
EXPECT_EQ((BI("200") - BI("500")).toString(), "-300");
EXPECT_EQ((BI("100") - BI("1")).toString(), "99"); // With borrow
}
TEST(Subtraction, MixedSigns) {
EXPECT_EQ((BI("200") - BI("-300")).toString(), "500");
EXPECT_EQ((BI("-200") - BI("300")).toString(), "-500");
EXPECT_EQ((BI("-500") - BI("-200")).toString(), "-300");
}
TEST(Subtraction, ResultingInZero) {
EXPECT_EQ((BI("200") - BI("200")).toString(), "0");
EXPECT_EQ((BI("-200") - BI("-200")).toString(), "0");
}
TEST(Multiplication, BasicCases) {
EXPECT_EQ((BI("12") * BI("10")).toString(), "120");
EXPECT_EQ((BI("123") * BI("456")).toString(), "56088");
EXPECT_EQ((BI("99") * BI("99")).toString(), "9801");
}
TEST(Multiplication, WithZero) {
EXPECT_EQ((BI("123") * BI("0")).toString(), "0");
EXPECT_EQ((BI("0") * BI("123")).toString(), "0");
EXPECT_EQ((BI("-123") * BI("0")).toString(), "0");
}
TEST(Multiplication, WithNegativeNumbers) {
EXPECT_EQ((BI("-12") * BI("10")).toString(), "-120");
EXPECT_EQ((BI("12") * BI("-10")).toString(), "-120");
EXPECT_EQ((BI("-12") * BI("-10")).toString(), "120");
}
TEST(DivisionAndRemainder, DivRemPositive) {
auto result = BigInt::divmod(BI("12345"), BI("54"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "228");
EXPECT_EQ(result->remainder.toString(), "33");
result = BigInt::divmod(BI("100"), BI("10"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "10");
EXPECT_EQ(result->remainder.toString(), "0");
}
TEST(DivisionAndRemainder, DivRemNegative) {
// -12345 / 54 -> Q = -228, R = -33
auto result = BigInt::divmod(BI("-12345"), BI("54"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "-228");
EXPECT_EQ(result->remainder.toString(), "-33");
// 12345 / -54 -> Q = -228, R = 33
result = BigInt::divmod(BI("12345"), BI("-54"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "-228");
EXPECT_EQ(result->remainder.toString(), "33");
// -12345 / -54 -> Q = 228, R = -33
result = BigInt::divmod(BI("-12345"), BI("-54"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "228");
EXPECT_EQ(result->remainder.toString(), "-33");
}
TEST(DivisionAndRemainder, QuotientOnly) {
auto q = BigInt::quotient(BI("56088"), BI("123"));
ASSERT_TRUE(q.has_value());
EXPECT_EQ(q->toString(), "456");
q = BigInt::quotient(BI("-100"), BI("9"));
ASSERT_TRUE(q.has_value());
EXPECT_EQ(q->toString(), "-11");
}
TEST(DivisionAndRemainder, RemainderOnly) {
auto r_pos = BigInt::remainder(BI("100"), BI("9"));
ASSERT_TRUE(r_pos.has_value());
EXPECT_EQ(r_pos->toString(), "1");
auto r_neg = BigInt::remainder(BI("-100"), BI("9"));
ASSERT_TRUE(r_neg.has_value());
EXPECT_EQ(r_neg->toString(), "-1");
}
TEST(DivisionAndRemainder, DivisionByZero) {
EXPECT_FALSE(BigInt::divmod(BI("100"), BI("0")).has_value());
EXPECT_FALSE(BigInt::quotient(BI("100"), BI("0")).has_value());
EXPECT_FALSE(BigInt::remainder(BI("100"), BI("0")).has_value());
}
TEST(DivisionAndRemainder, DividendSmallerThanDivisor) {
auto result = BigInt::divmod(BI("10"), BI("100"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "0");
EXPECT_EQ(result->remainder.toString(), "10");
result = BigInt::divmod(BI("-10"), BI("100"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "0");
EXPECT_EQ(result->remainder.toString(), "-10");
}
TEST(Comparison, AllOperators) {
EXPECT_TRUE(BI("100") > BI("99"));
EXPECT_TRUE(BI("99") < BI("100"));
EXPECT_TRUE(BI("-99") > BI("-100"));
EXPECT_TRUE(BI("-100") < BI("-99"));
EXPECT_TRUE(BI("100") >= BI("100"));
EXPECT_TRUE(BI("100") <= BI("100"));
EXPECT_TRUE(BI("100") == BI("100"));
EXPECT_TRUE(BI("100") != BI("101"));
EXPECT_TRUE(BI("100") > BI("-100"));
EXPECT_TRUE(BI("-100") < BI("100"));
}
TEST(LargeValues, HandlesValuesExceedingULLONG_MAX) {
// ULLONG_MAX is 18,446,744,073,709,551,615. These numbers are larger.
const auto big_num1 = BI("20000000000000000000"); // 2 * 10^19
const auto big_num2 = BI("10000000000000000000"); // 1 * 10^19
const auto neg_big_num1 = BI("-20000000000000000000");
// Addition
EXPECT_EQ((big_num1 + big_num2).toString(), "30000000000000000000");
// Subtraction
EXPECT_EQ((big_num1 - big_num2).toString(), "10000000000000000000");
// Multiplication
EXPECT_EQ((big_num1 * big_num2).toString(),
"200000000000000000000000000000000000000");
// Division and Remainder
auto result = BigInt::divmod(big_num1, big_num2);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(), "2");
EXPECT_EQ(result->remainder.toString(), "0");
// Comparisons
EXPECT_TRUE(big_num1 > big_num2);
EXPECT_TRUE(big_num2 < big_num1);
EXPECT_TRUE(big_num1 > neg_big_num1);
EXPECT_TRUE(neg_big_num1 < big_num2);
EXPECT_EQ((BI("7284792756374829238474859") * BI("28347299847928374918237"))
.toString(),
"206504204594973924026042468998840507198705103583");
result = BigInt::divmod(
BI("206504204594973924026042468998840507198705103583"), BI("38294791"));
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->quotient.toString(),
"5392488095704032541293735458664352214344");
EXPECT_EQ(result->remainder.toString(), "14421479");
}

420
common/bigint.cpp Normal file
View File

@ -0,0 +1,420 @@
#include "bigint.hpp"
#include <algorithm>
#include <ranges>
#include <format>
#include <print>
#include <sstream>
std::expected<BigInt, std::string>
BigInt::fromString(std::string_view value) {
BigInt result;
value = trim(value, isSpace);
if (value.empty()) {
return std::unexpected{"Invalid value: input string is empty."};
}
if (value.front() == '-') {
result.isNegative = true;
value.remove_prefix(1);
} else if (value.front() == '+') {
value.remove_prefix(1);
}
if (value.empty()) {
return std::unexpected{"Invalid value: string contains only a sign."};
}
if (rg::any_of(value, [](u8 ch) { return !std::isdigit(ch); })) {
return std::unexpected{"Invalid value: contains non-digit characters."};
}
result.digits.reserve(value.size() / 2 + 1);
auto it = value.begin();
if ((value.size() % 2) == 1) {
result.digits.push_back(u8(*it++ - '0'));
}
while (it != value.end()) {
u8 cval = u8((*it++ - '0') * 10);
cval += u8((*it++ - '0'));
result.digits.push_back(cval);
}
rg::reverse(result.digits);
result.trimLeadingZeroes();
if (result.isZero()) {
result.isNegative = false;
}
return result;
}
std::string BigInt::toString() const {
if (this->isZero()) {
return "0";
}
std::stringstream ss;
if (isNegative) {
ss << "-";
}
ss << i32(digits.back());
for (const auto &d : this->digits | vw::reverse | vw::drop(1)) {
ss << std::format("{:02}", i32(d));
}
return ss.str();
}
void BigInt::trimLeadingZeroes() noexcept {
while (!digits.empty() && digits.back() == 0) {
digits.pop_back();
}
}
BigInt BigInt::addMagnitudes(const BigInt &lhs, const BigInt &rhs) {
BigInt result;
u16 carry = 0;
const auto &longer = [&] -> const BigInt & {
if (lhs.size() > rhs.size()) {
return lhs;
}
return rhs;
}();
const auto &shorter = [&] -> const BigInt & {
if (lhs.size() <= rhs.size()) {
return lhs;
}
return rhs;
}();
result.digits.reserve(longer.size() + 1);
for (u64 i = 0; i < shorter.size(); ++i) {
u16 sum = shorter.digits[i] + longer.digits[i] + carry;
carry = sum / Base;
sum %= Base;
result.digits.push_back(sum);
}
for (u64 i = shorter.size(); i < longer.size(); ++i) {
u16 sum = longer.digits[i] + carry;
carry = sum / Base;
sum %= Base;
result.digits.push_back(sum);
}
if (carry != 0) {
result.digits.push_back(carry);
}
return result;
}
BigInt BigInt::subMagnitudes(const BigInt &lhs, const BigInt &rhs) {
BigInt result;
i16 borrow = 0;
result.digits.reserve(lhs.size() + 1);
for (u64 i = 0; i < rhs.size(); ++i) {
i16 diff = lhs.digits[i] - rhs.digits[i] - borrow;
if (diff < 0) {
diff += Base;
borrow = 1;
} else {
borrow = 0;
}
result.digits.push_back(u8(diff));
}
for (u64 i = rhs.size(); i < lhs.size(); ++i) {
i16 diff = lhs.digits[i] - borrow;
if (diff < 0) {
diff += Base;
borrow = 1;
} else {
borrow = 0;
}
result.digits.push_back(u8(diff));
}
result.trimLeadingZeroes();
return result;
}
BigInt BigInt::multiplyMagnitudes(const BigInt &lhs, const BigInt &rhs) {
if (lhs.isZero() || rhs.isZero()) {
return BigInt{};
}
std::vector<u32> accumulator(lhs.size() + rhs.size() + 1, 0);
const auto &longer = [&] -> const BigInt & {
if (lhs.size() > rhs.size()) {
return lhs;
}
return rhs;
}();
const auto &shorter = [&] -> const BigInt & {
if (lhs.size() <= rhs.size()) {
return lhs;
}
return rhs;
}();
for (const auto &[bottomIdx, bottom] : vw::enumerate(shorter.digits)) {
u32 carry = 0;
for (const auto &[topIdx, top] : vw::enumerate(longer.digits)) {
u32 prod = bottom * top + carry;
carry = prod / Base;
prod %= Base;
accumulator[bottomIdx + topIdx] += prod;
}
if (carry != 0) {
accumulator[longer.size() + bottomIdx] += carry;
}
}
BigInt result;
u32 carry = 0;
result.digits.reserve(accumulator.size() + 2);
for (auto &prod : accumulator) {
prod += carry;
carry = prod / Base;
prod %= Base;
result.digits.push_back(prod);
}
while (carry != 0) {
result.digits.push_back(carry % Base);
carry /= Base;
}
result.trimLeadingZeroes();
return result;
}
std::strong_ordering BigInt::compareMagnitudes(const BigInt &lhs,
const BigInt &rhs) noexcept {
if (lhs.size() > rhs.size())
return std::strong_ordering::greater;
if (lhs.size() < rhs.size())
return std::strong_ordering::less;
for (const auto &[left, right] :
vw::zip(lhs.digits, rhs.digits) | vw::reverse) {
if (left > right)
return std::strong_ordering::greater;
if (left < right)
return std::strong_ordering::less;
}
return std::strong_ordering::equal;
}
BigInt operator+(const BigInt &lhs, const BigInt &rhs) {
if (lhs.isNegative == rhs.isNegative) {
BigInt result = BigInt::addMagnitudes(lhs, rhs);
result.isNegative = lhs.isNegative;
return result;
}
auto comparison = BigInt::compareMagnitudes(lhs, rhs);
if (comparison == std::strong_ordering::greater) {
BigInt result = BigInt::subMagnitudes(lhs, rhs);
result.isNegative = lhs.isNegative;
return result;
}
if (comparison == std::strong_ordering::less) {
BigInt result = BigInt::subMagnitudes(rhs, lhs);
result.isNegative = rhs.isNegative;
return result;
}
return BigInt{};
}
BigInt operator-(const BigInt &lhs, const BigInt &rhs) {
BigInt negated_rhs = rhs;
if (!negated_rhs.isZero()) {
negated_rhs.isNegative = !rhs.isNegative;
}
return lhs + negated_rhs;
}
BigInt operator*(const BigInt &lhs, const BigInt &rhs) {
BigInt result = BigInt::multiplyMagnitudes(lhs, rhs);
result.isNegative = lhs.isNegative ^ rhs.isNegative;
if (result.isZero()) {
result.isNegative = false;
}
return result;
}
std::expected<BigInt::DivisionResult, std::string>
BigInt::divmod(const BigInt &lhs, const BigInt &rhs) {
DivisionResult result{};
if (rhs.isZero()) {
return std::unexpected{"Division by zero"};
}
if (lhs.isZero()) {
return result;
}
if (BigInt::compareMagnitudes(lhs, rhs) == std::strong_ordering::less) {
result.remainder = lhs;
return result;
}
auto &quotient = result.quotient;
auto &remainder = result.remainder;
remainder.digits.reserve(rhs.size());
for (const auto &digit : vw::reverse(lhs.digits)) {
remainder.digits.insert(remainder.digits.begin(), digit);
remainder.trimLeadingZeroes();
if (BigInt::compareMagnitudes(remainder, rhs) !=
std::strong_ordering::less) {
i16 quotDInt = 0;
BigInt quotD{};
for (i16 iter = Base - 1; iter >= 0; --iter) {
auto bigIter = BigInt::fromInteger(iter);
if (!bigIter) {
return std::unexpected{bigIter.error()};
}
auto mult = BigInt::multiplyMagnitudes(rhs, bigIter.value());
if (BigInt::compareMagnitudes(mult, remainder) !=
std::strong_ordering::greater) {
quotDInt = iter;
quotD = std::move(mult);
break;
}
}
remainder = BigInt::subMagnitudes(remainder, quotD);
quotient.digits.push_back(quotDInt);
}
}
rg::reverse(quotient.digits);
quotient.trimLeadingZeroes();
if (!quotient.isZero()) {
quotient.isNegative = lhs.isNegative ^ rhs.isNegative;
}
if (!remainder.isZero()) {
remainder.isNegative = lhs.isNegative;
}
return result;
}
std::expected<BigInt, std::string> BigInt::quotient(const BigInt &lhs,
const BigInt &rhs) {
auto result = BigInt::divmod(lhs, rhs);
if (result) {
return result->quotient;
}
return std::unexpected(result.error());
}
std::expected<BigInt, std::string> BigInt::remainder(const BigInt &lhs,
const BigInt &rhs) {
auto result = BigInt::divmod(lhs, rhs);
if (result) {
return result->remainder;
}
return std::unexpected(result.error());
}
std::strong_ordering operator<=>(const BigInt &lhs, const BigInt &rhs) {
if (lhs.isZero() && rhs.isZero()) {
return std::strong_ordering::equal;
}
if (lhs.isNegative && !rhs.isNegative) {
return std::strong_ordering::less;
}
if (!lhs.isNegative && rhs.isNegative) {
return std::strong_ordering::greater;
}
auto magnitude_comparison = BigInt::compareMagnitudes(lhs, rhs);
// If both are negative, the ordering is the reverse of their magnitude.
// e.g., magnitude of -100 > magnitude of -90, but -100 < -90.
if (lhs.isNegative) {
if (magnitude_comparison == std::strong_ordering::less)
return std::strong_ordering::greater;
if (magnitude_comparison == std::strong_ordering::greater)
return std::strong_ordering::less;
return std::strong_ordering::equal;
}
return magnitude_comparison;
}
bool operator==(const BigInt &lhs, const BigInt &rhs) {
return (lhs <=> rhs) == std::strong_ordering::equal;
}
bool operator!=(const BigInt &lhs, const BigInt &rhs) {
return !(lhs == rhs);
}

86
common/bigint.hpp Normal file
View File

@ -0,0 +1,86 @@
#pragma once
#include <expected>
#include <string>
#include <string_view>
#include <vector>
#include <strings.hpp>
#include <types.hpp>
struct BigInt {
static constexpr u32 Base = 100;
bool isNegative = false;
std::vector<u8> digits;
struct DivisionResult;
friend BigInt operator+(const BigInt &, const BigInt &);
friend BigInt operator-(const BigInt &, const BigInt &);
friend BigInt operator*(const BigInt &, const BigInt &);
friend std::strong_ordering operator<=>(const BigInt &lhs, const BigInt &rhs);
static std::expected<BigInt, std::string>
fromString(std::string_view value);
static std::expected<BigInt, std::string>
fromInteger(std::integral auto value) {
auto str = std::to_string(value);
return BigInt::fromString(str);
}
std::string toString() const;
inline bool isZero() const noexcept {
return digits.empty();
}
BigInt() = default;
BigInt(BigInt &&) = default;
BigInt(const BigInt &) = default;
BigInt &operator=(BigInt &&) = default;
BigInt &operator=(const BigInt &) = default;
~BigInt() = default;
static std::expected<DivisionResult, std::string> divmod(const BigInt &lhs,
const BigInt &rhs);
static std::expected<BigInt, std::string> quotient(const BigInt &lhs,
const BigInt &rhs);
static std::expected<BigInt, std::string> remainder(const BigInt &lhs,
const BigInt &rhs);
private:
void trimLeadingZeroes() noexcept;
inline u64 size() const noexcept {
return this->digits.size();
}
static BigInt addMagnitudes(const BigInt &lhs, const BigInt &rhs);
static BigInt subMagnitudes(const BigInt &lhs, const BigInt &rhs);
static BigInt multiplyMagnitudes(const BigInt &lhs, const BigInt &rhs);
static std::strong_ordering compareMagnitudes(const BigInt &lhs,
const BigInt &rhs) noexcept;
};
struct BigInt::DivisionResult {
BigInt quotient;
BigInt remainder;
};
BigInt operator+(const BigInt &lhs, const BigInt &rhs);
BigInt operator-(const BigInt &lhs, const BigInt &rhs);
BigInt operator*(const BigInt &lhs, const BigInt &rhs);
std::strong_ordering operator<=>(const BigInt &lhs, const BigInt &rhs);
bool operator==(const BigInt &lhs, const BigInt &rhs);
bool operator!=(const BigInt &lhs, const BigInt &rhs);

44
common/strings.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <cctype>
#include <functional>
#include <string_view>
static inline bool isSpace(char ch) {
return std::isspace(ch);
}
static std::string_view ltrim(std::string_view str,
std::function<bool(char)> filter = isSpace) {
auto it = str.begin();
while (it != str.end()) {
if (!filter(*it)) {
break;
}
++it;
}
return {it, str.end()};
}
static std::string_view rtrim(std::string_view str,
std::function<bool(char)> filter = isSpace) {
auto it = str.rbegin();
while (it != str.rend()) {
if (!filter(*it)) {
break;
}
++it;
}
return {str.begin(), it.base()};
}
static std::string_view trim(std::string_view str,
std::function<bool(char)> filter = isSpace) {
return ltrim(rtrim(str, filter), filter);
}

20
common/types.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <ranges>
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using u64 = uint64_t;
using i8 = int8_t;
using i16 = int16_t;
using i32 = int32_t;
using i64 = int64_t;
using f32 = float;
using f64 = double;
namespace rg = std::ranges;
namespace vw = std::views;