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+). Цифры — только ASCII0-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.5 → | 3.5 → | −2.5 → | 2.4 → | 2.6 → |
|---|---|---|---|---|---|---|
HalfEven | ровно половина → к чётному соседу (банковское округление) | 2 | 4 | −2 | 2 | 3 |
HalfUp | ровно половина → от нуля (школьное округление) | 3 | 4 | −3 | 2 | 3 |
HalfDown | ровно половина → к нулю | 2 | 3 | −2 | 2 | 3 |
Down | всегда к нулю (усечение, хвост игнорируется) | 2 | 3 | −2 | 2 | 2 |
Up | всегда от нуля (если хвост ненулевой) | 3 | 4 | −3 | 3 | 3 |
Ceiling | всегда к +∞ | 3 | 4 | −2 | 3 | 3 |
Floor | всегда к -∞ | 2 | 3 | −3 | 2 | 2 |
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) -> str — scale_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— полный набор тестов