← Документация · nv-lang/nova-bignum

BigDecimal

BigDecimal — десятичное число произвольной точности, модуль bignum.bigdecimal. Сложение, вычитание и умножение — обычными операторами +/-/*; деление требует явной точности (см. ниже). Под капотом каждая операция делегируется в BigInt. Как он соотносится с остальной семьёй bignum — см. overview.ru.md.

Представление

type BigDecimal value {
    mant BigInt
    scale int
}

Значение = mant × 10^{-scale}. scale может быть отрицательным (целое с неявными конечными нулями). Копия — указатель на BigInt плюс int — примерно 16 байт на стеке; сами данные мантиссы разделяются, как у любого BigInt.

Построение

ro price = "19.99".to_bigdecimal()!!
ro zero = BigDecimal.zero()
ro direct = BigDecimal.new(1999.to_bigint(), 2)
  • str @to_bigdecimal() -> Result[BigDecimal, ParseNumberError] — парсит [sign] (int-part ['.' [frac-part]] | '.' frac-part) [exp-part], например "19.99", ".5", "5.", "1e-3", "1.5E+2". _ допускается как разделитель групп разрядов и удаляется до вычисления scale (стиль Rust/Python 3.6+). Цифры — только ASCII 0-9, всё остальное — Err(InvalidCharacter).
  • T @to_bigdecimal() для любого члена Ints плюс i128 — всегда scale = 0, бесотказная.
  • BigDecimal.new(mant, scale) — прямое построение, без нормализации.
  • BigDecimal.zero() — каноничный ноль (mant = 0, scale = 0).

@scale() -> int читает поле scale (свойство-читатель, не операция округления — см. @rescale ниже, у которой отдельное имя именно чтобы не конфликтовать с этим читателем).

Арифметика

ro a = "0.1".to_bigdecimal()!!
ro b = "0.2".to_bigdecimal()!!
ro c = a + b
assert(c.to_str(scale_pad: 0) == "0.3")

+/-/* десугарятся в @plus/@minus/@times; @neg/@abs точные. Сложение и вычитание выравнивают scale (расширяют операнд с меньшим scale на нужную степень десяти) перед объединением мантисс; умножение просто складывает scale — ни одна из трёх операций никогда не округляет.

ro price = "19.99".to_bigdecimal()!!
ro tax = "0.01".to_bigdecimal()!!
assert((price + tax).to_str() == "20.00")     // not 20.000000000000004, like f64

Оператор / НЕ десугарится — деление двух BigDecimal неоднозначно без явной точности, поэтому деление доступно только как @div(other, MathContext):

ro third = 1.to_bigdecimal().div(3.to_bigdecimal(), MathContext.new(4, HalfEven))
assert(third.to_str() == "0.3333")

Деление на ноль паникует (requires !other.mant.is_zero()) — паритет с контрактом деления-на-ноль примитивных int/f64 (D423), не Result.

Округление: RoundingMode и MathContext

type RoundingMode enum HalfEven | HalfUp | HalfDown | Down | Up | Ceiling | Floor

type MathContext value {
    precision int
    rm RoundingMode
}

Все семь режимов совпадают, когда отброшенный хвост НЕ равен ровно половине: ниже половины — все усекают, выше половины — все округляют от нуля. Расходятся они только ровно на границе «половина», а для Up/Down/Ceiling/Floor — ещё и в том, куда считается «от нуля». Округление до ближайшего целого:

РежимПравило2.53.5−2.52.42.6
HalfEvenровно половина → к чётному соседу (банковское округление)24−223
HalfUpровно половина → от нуля (школьное округление)34−323
HalfDownровно половина → к нулю23−223
Downвсегда к нулю (усечение, хвост игнорируется)23−222
Upвсегда от нуля (если хвост ненулевой)34−333
Ceilingвсегда к +∞34−233
Floorвсегда к -∞23−322

HalfEven — режим по умолчанию, к которому стоит тянуться: это дефолт IEEE 754 и то, что обычно нужно для денежных расчётов, потому что округления вверх и вниз происходят поровну и систематическая ошибка не накапливается на множестве округлений. Режим задаётся при каждом вызове через MathContext.new(precision, rm) — неявного значения по умолчанию нет.

ro half = "2.5".to_bigdecimal()!!
ro even = half.rescale(0, HalfEven)     // 2 — 2.5 ties to the even neighbor
ro up = half.rescale(0, HalfUp)         // 3 — 2.5 always rounds away from zero
assert(even.to_str() == "2")
assert(up.to_str() == "3")
  • MathContext.new(precision, rm)precision считает значащие цифры мантиссы, не десятичные знаки; паникует (requires precision >= 1) ниже 1 — неограниченная точность (0) в V1 не поддержана, потому что неограниченное 1/3 никогда не завершится.
ro a = 2.to_bigdecimal()
ro b = 3.to_bigdecimal()
ro mc = MathContext.new(5, HalfUp)
assert(a.div(b, mc).to_str(scale_pad: 0) == "0.66667")

@round(ctx MathContext) -> BigDecimal округляет до ctx.precision значащих цифр. @rescale(target int, rm RoundingMode) -> BigDecimal — scale-ориентированный аналог (соответствует Java setScale(int, RoundingMode)): target > scale дополняет нулями без округления, target < scale округляет, отбрасывая десятичные знаки, а target < 0 округляет до 10^|target| (например, target = -2 округляет до ближайшей сотни).

Сравнение и равенство

ro x = BigDecimal.new(10.to_bigint(), 1)   // 1.0
ro y = BigDecimal.new(1.to_bigint(), 0)    // 1
assert(x.compare(y) == 0)
assert(x == y)

@compare(other) -> int НЕ нормализует — выравнивает scale (домножает операнд с меньшим scale на подходящую степень десяти) и сравнивает мантиссы напрямую. @equal(other) (за которым стоит ==) вместо этого сначала нормализует оба операнда — паритет с крейтом bigdecimal из Rust, где 1.0 == 1. Эта асимметрия осознанна: @compare никогда не тратит проход нормализации на горячем пути, @equal без каноничной формы просто некорректен. @hash() согласован с @equal (тоже нормализует).

Нормализация

@normalize() -> BigDecimal убирает конечные десятичные нули мантиссы, уменьшая scale соответственно — она lazy: ни один конструктор или арифметическая операция не вызывает её неявно, только @equal/@hash/ явный вызов. Это держит арифметику дешёвой (без O(n²)-цикла деления на 10 на каждой операции) ценой того, что значения BigDecimal не в единой каноничной форме, пока вы не запросите её явно.

Строковый вывод

ro x = "12.5".to_bigdecimal()!!
assert(x.to_str() == "12.5")
assert(x.to_str(scale_pad: 4) == "12.5000")

@to_str(scale_pad int = 0) -> strscale_pad — минимум цифр после запятой (дополнение справа нулями; 0 — без дополнения). Знак печатается первым, перед любым ведущим нулём, поэтому -0.5 корректно round-trip’ится.

Конверсии в фиксированные типы

@to_int() -> Option[int] и @to_i128() -> Option[i128] сначала нормализуют: положительный остаточный scale после нормализации (настоящая дробная часть) даёт None вместо молчаливого усечения; отрицательный scale (целое с неявными конечными нулями) материализуется перед проверкой диапазона.

Чего нет в BigDecimal V1

pow/sqrt, сам оператор /, мутабельные методы @*_assign, цепные операции с авто-переносом точности, интеграция с generic-числовыми type-sets, неявные коэрсии/литералы, кэш малых степеней 10, умножение Toom-Cook (унаследовано от базового BigInt).

Связанные документы

  • overview.ru.md — карта семьи bignum, общее правило точных/теряющих точность конверсий
  • bigint.ru.md — тип, которому в итоге делегирует каждая операция BigDecimal
  • bigfloat.ru.md — мост BigDecimal ↔ BigFloat (через PrecisionContext)
  • bigrat.ru.md — точный мост BigDecimal ↔ BigRat
  • src/bigdecimal/core.nv — полный исходник
  • src/bigdecimal/core_test.nv — полный набор тестов