// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // IntIO.cpp // Implements input/output operations for multi-precision integers #include #include #include #include #include #include #include #include #include #include #include #include namespace sangi { std::string IntIOUtils::fromRawWords(const std::vector& words, int sign) { if (words.empty()) { return "0"; } // Create a new vector with leading zeros removed std::vector trimmed_words; auto it = words.rbegin(); // Find the first non-zero value while (it != words.rend() && *it == 0) { ++it; } // Copy the remaining valid values while (it != words.rend()) { trimmed_words.push_back(*it); ++it; } if (trimmed_words.empty()) { return "0"; } std::string result; // Generate the hexadecimal representation for (auto it = trimmed_words.begin(); it != trimmed_words.end(); ++it) { std::stringstream ss; ss << std::hex << std::setfill('0'); // Zero-pad to 16 digits for all values except the first if (it != trimmed_words.begin()) { ss << std::setw(16); } ss << *it; result += ss.str(); } // Remove leading zeros result.erase(0, result.find_first_not_of('0')); if (result.empty()) { return "0"; } // Add the sign if (sign < 0) { result = "-" + result; } return result; } // ================================================================ // Decimal conversion: small-size naive + large-size divide-and-conquer // ================================================================ // // Small size (< DC_TOSTRING_THRESHOLD limbs): // Extract 18 digits at a time by repeating divmod_1(10^18). O(n²) but // with a small constant, avoiding the DC overhead (multi-precision divmod, pow10 composition). // // Large size: // Divide-and-conquer O(M(N) log N). Pre-allocate the output buffer and // write directly at each recursion level (avoiding string concatenation). static constexpr double LOG10_2 = 0.30102999566398120; static constexpr uint64_t POW10_18 = 1000000000000000000ULL; // 10^18 static constexpr size_t DIGITS_PER_CHUNK = 18; // DC threshold: below this, use naive (repeated divmod_1) static constexpr size_t DC_TOSTRING_THRESHOLD = 30; // Estimate the number of decimal digits from the bit count static size_t estimateDecimalDigits(size_t bitLength) { return static_cast(static_cast(bitLength) * LOG10_2) + 1; } // Cache of 10^k (built by repeated squaring) // cache[i] = 10^(2^i) — i=0: 10, i=1: 100, i=2: 10000, ... static std::vector s_pow10_cache; // Compute 10^n efficiently (with caching) static Int pow10_cached(size_t n) { if (n == 0) return Int(1); // Build the cache: s_pow10_cache[i] = 10^(2^i) while (s_pow10_cache.empty() || (size_t(1) << (s_pow10_cache.size() - 1)) < n) { if (s_pow10_cache.empty()) { s_pow10_cache.push_back(Int(10)); // 10^1 } else { const Int& prev = s_pow10_cache.back(); s_pow10_cache.push_back(prev * prev); // 10^(2^k) = (10^(2^(k-1)))^2 } } // Decompose n in binary and compute the product Int result(1); size_t remaining = n; for (size_t i = 0; remaining > 0 && i < s_pow10_cache.size(); i++) { if (remaining & (size_t(1) << i)) { result = result * s_pow10_cache[i]; remaining &= ~(size_t(1) << i); } } return result; } // Write a uint64_t as an 18-digit string into the buffer static void u64_to_18digits(uint64_t v, char* out) { // Generate 18 digits from the back for (int i = 17; i >= 0; --i) { out[i] = '0' + static_cast(v % 10); v /= 10; } } // Write a uint64_t as a variable-length string into the buffer, returning the digit count static size_t u64_to_digits(uint64_t v, char* out) { if (v == 0) { out[0] = '0'; return 1; } char tmp[20]; size_t len = 0; while (v > 0) { tmp[len++] = '0' + static_cast(v % 10); v /= 10; } for (size_t i = 0; i < len; i++) out[i] = tmp[len - 1 - i]; return len; } // Naive conversion: decompose into 18-digit chunks by repeating divmod_1(10^18) // For small sizes (< DC_TOSTRING_THRESHOLD limbs) static std::string toDecimalNaive(const uint64_t* data, size_t n) { // Remove leading zeros while (n > 0 && data[n - 1] == 0) --n; if (n == 0) return "0"; // 1-limb fast path: convert directly with snprintf if (n == 1) { char buf[21]; int len = std::snprintf(buf, sizeof(buf), "%" PRIu64, data[0]); return std::string(buf, static_cast(len)); } // Working copy (divmod_1 writes the quotient in-place) // For small sizes, use a stack buffer to avoid heap allocation constexpr size_t STACK_LIMIT = 64; uint64_t stack_buf[STACK_LIMIT]; uint64_t* work_ptr; std::vector work_vec; if (n <= STACK_LIMIT) { std::memcpy(stack_buf, data, n * sizeof(uint64_t)); work_ptr = stack_buf; } else { work_vec.assign(data, data + n); work_ptr = work_vec.data(); } // Collect 18-digit chunks in reverse order // Maximum chunk count: ceil(n * 64 * log10(2) / 18) + 1 std::vector chunks; chunks.reserve(n * 4); // ample margin while (n > 0) { uint64_t rem = mpn::divmod_1(work_ptr, work_ptr, n, POW10_18); chunks.push_back(rem); while (n > 0 && work_ptr[n - 1] == 0) --n; } // The leading chunk is variable-length; the rest are fixed at 18 digits size_t totalLen = 20 + (chunks.size() - 1) * DIGITS_PER_CHUNK; std::string result; result.resize(totalLen); char* p = result.data(); // Most significant chunk (no zero padding) size_t headLen = u64_to_digits(chunks.back(), p); p += headLen; // Remaining chunks (18-digit zero padding) for (size_t i = chunks.size() - 1; i-- > 0; ) { u64_to_18digits(chunks[i], p); p += DIGITS_PER_CHUNK; } result.resize(static_cast(p - result.data())); return result; } // DC recursion body: writes into buf[pos..pos+padDigits-1] // When padDigits == 0 (most significant): no leading zeros; returns the position after writing static void decimalSplitRec(const Int& x, char* buf, size_t pos, size_t padDigits) { // Base case: delegate to naive conversion size_t n = x.size(); if (n < DC_TOSTRING_THRESHOLD) { if (x.isZero()) { // Write padDigits zeros std::memset(buf + pos, '0', padDigits); return; } // Stringify with naive and copy into the buffer std::string s = toDecimalNaive(x.data(), n); if (padDigits > s.size()) { size_t pad = padDigits - s.size(); std::memset(buf + pos, '0', pad); std::memcpy(buf + pos + pad, s.data(), s.size()); } else { std::memcpy(buf + pos, s.data(), s.size()); } return; } // Split at half of padDigits size_t half = padDigits / 2; size_t upperPad = padDigits - half; // upper takes the remainder (guarantees half + upperPad == padDigits) // Compute 10^half with caching Int divisor = pow10_cached(half); Int R; Int Q = IntOps::divmod(x, divisor, R); // Upper half: upperPad digits starting at pos decimalSplitRec(Q, buf, pos, upperPad); // Lower half: half digits starting at pos + upperPad decimalSplitRec(R, buf, pos + upperPad, half); } // Decimal string conversion static std::string toDecimalString(const Int& value) { if (value.isZero()) return "0"; bool isNeg = value.isNegative(); size_t n = value.size(); // Small size: naive (repeated divmod_1) — no Int copy needed, references data() directly if (n < DC_TOSTRING_THRESHOLD) { std::string result = toDecimalNaive(value.data(), n); if (isNeg) result.insert(0, 1, '-'); return result; } // Large size: absVal is needed (DC splits the Int) Int absVal = isNeg ? -value : value; n = absVal.size(); // Large size: DC (pre-allocated buffer) // estimateDecimalDigits is ceil(bits * log10(2)) + 1, which can be 1 digit too many. // Allocate the buffer with margin and verify the exact digit count via naive conversion after DC conversion. size_t estDigits = estimateDecimalDigits(absVal.bitLength()); size_t bufSize = estDigits + 4; std::string buf(bufSize, '0'); // Write into the buffer via DC recursion decimalSplitRec(absVal, buf.data(), 0, estDigits); // Remove leading zeros size_t start = 0; while (start < buf.size() - 1 && buf[start] == '0') ++start; // Tail: within the estDigits-wide buffer, the result is the part excluding leading zeros. // However, if estDigits is 1-2 digits too many, extra zeros are appended at the tail. // Exact digit count: floor(log10(value)) + 1 = integer part of bitLength * log10(2) + 1 // Computing the leading few digits with naive to determine the exact count is costly, so // instead: estDigits digits if value >= 10^(estDigits-1), otherwise estDigits-1 digits size_t actualDigits = estDigits; if (estDigits > 1) { Int threshold = pow10_cached(estDigits - 1); if (absVal < threshold) { actualDigits = estDigits - 1; } } // Extract actualDigits digits from the buffer starting at start std::string result = buf.substr(start, actualDigits); if (isNeg) { result.insert(0, 1, '-'); } return result; } // ================================================================ // General-purpose toString (arbitrary base) // ================================================================ // For bases other than 10, use the conventional sequential divmod (for small numbers) static std::string toStringNaive(const Int& value, int base) { static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; Int x = value; bool isNeg = x.isNegative(); if (isNeg) x = -x; std::string result; Int baseInt(base); while (!x.isZero()) { Int remainder; x = IntOps::divmod(x, baseInt, remainder); result.push_back(digits[remainder.toInt()]); } std::reverse(result.begin(), result.end()); if (isNeg) result.insert(0, 1, '-'); return result; } std::string IntIOUtils::toString(const Int& value, int base) { // Handle special states std::string specialResult = IntSpecialStates::handleToString(value); if (!specialResult.empty()) { return specialResult; } if (base < 2 || base > 36) { throw std::invalid_argument("Base must be between 2 and 36"); } if (value.m_sign == 0) { return "0"; } // base == 10: divide-and-conquer (O(M(N) * log N)) if (base == 10) { return toDecimalString(value); } // Other bases: conventional sequential conversion return toStringNaive(value, base); } // ================================================================ // fromString internal helpers // ================================================================ // Convert a single character to a numeric value (invalid character: -1, separator: -2) static int charToDigit(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'z') return c - 'a' + 10; if (c >= 'A' && c <= 'Z') return c - 'A' + 10; if (c == '_' || c == '\'') return -2; // separator return -1; // invalid character } // Validate the digit portion and return a clean string with separators removed static std::string cleanDigits(std::string_view str, size_t start, int base) { std::string digits; digits.reserve(str.size() - start); for (size_t i = start; i < str.size(); ++i) { int d = charToDigit(str[i]); if (d == -2) continue; // skip separators if (d < 0) { throw std::invalid_argument( "Invalid character in string: " + std::string(1, str[i])); } if (d >= base) { throw std::invalid_argument("Digit out of range for specified base"); } digits.push_back(str[i]); } return digits; } // Return the bit count for a power-of-two base (0 if not a power of two) static int bitsForPow2Base(int base) { switch (base) { case 2: return 1; case 4: return 2; case 8: return 3; case 16: return 4; case 32: return 5; default: return 0; } } // ---------------------------------------------------------------- // (1) Bit packing: power-of-two bases (2, 4, 8, 16, 32) // no multiplication needed, O(n) // ---------------------------------------------------------------- static Int fromStringPow2(const std::string& digits, int bitsPerDigit) { if (digits.empty()) return Int::Zero(); // Compute the word count from the total bit count size_t totalBits = digits.size() * static_cast(bitsPerDigit); size_t numWords = (totalBits + 63) / 64; // Allocate the word array std::vector words(numWords, 0); // Process from the back (least significant digit) and pack in little-endian size_t bitPos = 0; for (size_t i = digits.size(); i-- > 0; ) { int d = charToDigit(digits[i]); size_t wordIdx = bitPos / 64; size_t bitIdx = bitPos % 64; words[wordIdx] |= static_cast(d) << bitIdx; // When bitsPerDigit straddles a word boundary if (bitIdx + bitsPerDigit > 64) { words[wordIdx + 1] |= static_cast(d) >> (64 - bitIdx); } bitPos += bitsPerDigit; } // Remove leading zero words while (!words.empty() && words.back() == 0) { words.pop_back(); } if (words.empty()) return Int::Zero(); return Int::fromRawWords(words, 1); } // ---------------------------------------------------------------- // (2) Horner's method: small digit counts of a general base // compute result = result * base + digit sequentially // ---------------------------------------------------------------- // Chunked Horner's method: process up to chunk_size digits at a time // For base=10, 18 digits at a time (10^18 < 2^64) reduces to multi-precision × 1-limb multiplication static Int fromStringHorner(const std::string& digits, int base) { if (digits.empty()) return Int::Zero(); // Compute the largest k such that base^k fits in a uint64_t int chunk_size = 1; uint64_t chunk_base = static_cast(base); { uint64_t limit = UINT64_MAX / base; uint64_t b = chunk_base; while (b <= limit) { b *= base; chunk_size++; } chunk_base = b / base; // base^(chunk_size-1) chunk_size--; // chunk_base = base^chunk_size chunk_base = 1; for (int i = 0; i < chunk_size; i++) chunk_base *= base; } Int result = Int::Zero(); size_t pos = 0; size_t len = digits.size(); // First partial chunk (len % chunk_size digits) size_t first_chunk = len % chunk_size; if (first_chunk == 0 && len > 0) first_chunk = chunk_size; { uint64_t v = 0; for (size_t i = 0; i < first_chunk; i++) { v = v * base + charToDigit(digits[pos++]); } result = Int(v); } // Process the rest chunk_size digits at a time // multiplyWord + addWord completely eliminates temporary Int creation // result is built from Int(v) → always Normal → Unchecked is safe while (pos < len) { uint64_t v = 0; for (int i = 0; i < chunk_size; i++) { v = v * base + charToDigit(digits[pos++]); } IntOps::multiplyWordUnchecked(result, chunk_base); IntOps::addWord(result, v); } return result; } // ---------------------------------------------------------------- // (3) Divide-and-conquer: large digit counts of a general base // split the string in half, upper * base^(lower digit count) + lower // complexity: O(M(n) * log n) // ---------------------------------------------------------------- // Threshold between Horner and divide-and-conquer (based on decimal digit count) // Lowered the threshold because chunked Horner sped up the base case static size_t FROMSTRING_DC_THRESHOLD = 400; static Int fromStringDC(const std::string& digits, int base) { if (digits.size() <= FROMSTRING_DC_THRESHOLD) { return fromStringHorner(digits, base); } size_t half = digits.size() / 2; size_t lowerLen = digits.size() - half; // Upper half digits, lower lowerLen digits std::string upperStr = digits.substr(0, half); std::string lowerStr = digits.substr(half); Int upper = fromStringDC(upperStr, base); Int lower = fromStringDC(lowerStr, base); // upper * base^lowerLen + lower Int basePow = pow(Int(base), static_cast(lowerLen)); return upper * basePow + lower; } // ================================================================ // fromString body // ================================================================ Int IntIOUtils::fromString(std::string_view str, int base) { if (base != 0 && (base < 2 || base > 36)) { throw std::invalid_argument("Base must be between 2 and 36"); } // Empty string check if (str.empty()) { throw std::invalid_argument("Cannot convert empty string to Int"); } // Handle special strings if (str == "NaN") return Int::NaN(); if (str == "Infinity" || str == "+Infinity") return Int::PositiveInfinity(); if (str == "-Infinity") return Int::NegativeInfinity(); // Handle the sign bool isNegative = false; size_t start = 0; if (str[0] == '-') { isNegative = true; start = 1; } else if (str[0] == '+') { start = 1; } // Handle the base prefix if (base == 0) { if (str.size() > start + 1 && str[start] == '0') { if (str[start + 1] == 'x' || str[start + 1] == 'X') { base = 16; start += 2; } else if (str[start + 1] == 'b' || str[start + 1] == 'B') { base = 2; start += 2; } else { base = 8; start += 1; } } else { base = 10; } } // Decimal small fast path: manual parse for 20 digits or fewer (avoids cleanDigits) // 18 digits or fewer: no overflow (10^18 < UINT64_MAX) // 19-20 digits: with overflow checking { size_t ndigits = str.size() - start; if (base == 10 && ndigits <= 20) { bool clean = true; for (size_t i = start; i < str.size(); i++) { char c = str[i]; if (c < '0' || c > '9') { clean = false; break; } } if (clean) { uint64_t val = 0; bool overflow = false; for (size_t i = start; i < str.size(); i++) { uint64_t d = str[i] - '0'; if (ndigits >= 20) { // Overflow check if (val > UINT64_MAX / 10 || (val == UINT64_MAX / 10 && d > UINT64_MAX % 10)) { overflow = true; break; } } val = val * 10 + d; } if (!overflow) { if (val == 0) return Int::Zero(); Int result(val); if (isNegative) result.negate(); return result; } // overflow → fall through to the normal path } } } // Validate and clean the digit portion std::string digits = cleanDigits(str, start, base); if (digits.empty()) return Int::Zero(); // Algorithm selection Int result; int bitsPerDigit = bitsForPow2Base(base); if (bitsPerDigit > 0) { // Power-of-two base: bit packing O(n) result = fromStringPow2(digits, bitsPerDigit); } else if (digits.size() <= FROMSTRING_DC_THRESHOLD) { // General base, small digit count: Horner's method result = fromStringHorner(digits, base); } else { // General base, large digit count: divide-and-conquer result = fromStringDC(digits, base); } // Apply the sign if (isNegative) { result = -result; } return result; } std::string IntIOUtils::format(const Int& value, const FormatOptions& options) { // Handle special states std::string specialResult = IntSpecialStates::handleToString(value); if (!specialResult.empty()) { return specialResult; } // Handle normal values std::stringstream ss; // Handle the sign bool isNegative = value.m_sign < 0; // Handle based on the sign display setting if (options.showSign && !isNegative && value.m_sign != 0) { ss << "+"; } else if (isNegative) { ss << "-"; } // Display the base prefix if (options.showBase) { switch (options.base) { case 2: ss << "0b"; break; case 8: ss << "0"; break; case 16: ss << "0x"; break; // No special prefix for other bases } } // Uppercase/lowercase setting std::string rawString = toString(isNegative ? -value : value, options.base); // Handle minimum width and zero padding if (options.width > 0 && rawString.length() < static_cast(options.width)) { size_t padSize = options.width - rawString.length(); if (options.zeroPad) { rawString = std::string(padSize, '0') + rawString; } else { rawString = std::string(padSize, ' ') + rawString; } } // Uppercase/lowercase conversion if (options.uppercase) { std::transform(rawString.begin(), rawString.end(), rawString.begin(), [](unsigned char c) { return std::toupper(c); }); } ss << rawString; return ss.str(); } void IntIOUtils::setFromStringDCThreshold(size_t threshold) { FROMSTRING_DC_THRESHOLD = threshold; } size_t IntIOUtils::getFromStringDCThreshold() { return FROMSTRING_DC_THRESHOLD; } // ================================================================ // exportBinary / importBinary // ================================================================ static inline bool is_native_little_endian() { uint16_t x = 1; return *reinterpret_cast(&x) == 1; } static inline uint64_t swap64(uint64_t v) { return ((v & 0x00000000000000FFULL) << 56) | ((v & 0x000000000000FF00ULL) << 40) | ((v & 0x0000000000FF0000ULL) << 24) | ((v & 0x00000000FF000000ULL) << 8) | ((v & 0x000000FF00000000ULL) >> 8) | ((v & 0x0000FF0000000000ULL) >> 24) | ((v & 0x00FF000000000000ULL) >> 40) | ((v & 0xFF00000000000000ULL) >> 56); } static inline uint64_t to_target_endian(uint64_t v, IntIOUtils::Endian endian) { bool target_le = (endian == IntIOUtils::Endian::Little) || (endian == IntIOUtils::Endian::Native && is_native_little_endian()); bool native_le = is_native_little_endian(); if (target_le == native_le) return v; return swap64(v); } std::vector IntIOUtils::exportBinary(const Int& value, Endian endian) { if (value.isNaN() || value.isInfinite()) { throw std::invalid_argument("exportBinary: NaN/Infinity cannot be serialized"); } uint32_t n = 0; uint8_t sign_byte = 0; // 0 = zero if (!value.isZero()) { n = static_cast(value.size()); sign_byte = value.isNegative() ? 0xFF : 0x01; } // Header: sign(1) + size(4) + data: n*8 std::vector result(5 + static_cast(n) * 8); result[0] = sign_byte; // Write size as 4 LE bytes std::memcpy(&result[1], &n, 4); // Write the words (LSW first) const uint64_t* words = value.data(); for (uint32_t i = 0; i < n; ++i) { uint64_t w = to_target_endian(words[i], endian); std::memcpy(&result[5 + i * 8], &w, 8); } return result; } Int IntIOUtils::importBinary(std::span data, Endian endian) { if (data.size() < 5) { throw std::invalid_argument("importBinary: data too short (need at least 5 bytes)"); } uint8_t sign_byte = data[0]; uint32_t n; std::memcpy(&n, &data[1], 4); if (data.size() < 5 + static_cast(n) * 8) { throw std::invalid_argument("importBinary: data too short for declared word count"); } if (n == 0 || sign_byte == 0) { return Int(0); } std::vector words(n); for (uint32_t i = 0; i < n; ++i) { uint64_t w; std::memcpy(&w, &data[5 + i * 8], 8); words[i] = to_target_endian(w, endian); } int sign = (sign_byte == 0xFF) ? -1 : 1; return Int::fromRawWords(words, sign); } } // namespace sangi