Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
## Description
`math.decimal` provides arbitrary-precision fixed-point decimal arithmetic.
It stores values as an integer coefficient plus a decimal scale, so addition,
subtraction, and multiplication stay exact.
Division is exposed through `div_prec`, which rounds half up to a caller-chosen
number of decimal places. The `/` operator uses
`decimal.default_division_precision`.
## Example
```v
import math.decimal
price := decimal.from_string('19.99')!
fee := decimal.from_string('1.50')!
total := price + fee
assert total.str() == '21.49'
share := total.div_prec(decimal.from_int(3), 2)!
assert share.str() == '7.16'
```
+256
View File
@@ -0,0 +1,256 @@
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module decimal
import math.big
import strings
// default_division_precision is used by the `/` operator when the quotient
// needs a rounded decimal expansion.
pub const default_division_precision = 16
// zero is the canonical zero value for Decimal.
pub const zero = Decimal{
coefficient_value: big.zero_int
scale_value: 0
}
// Decimal represents an arbitrary-precision fixed-point decimal value.
// Its numeric value is `coefficient * 10^-scale`.
pub struct Decimal {
coefficient_value big.Integer
scale_value int
}
// new creates a decimal from `coefficient * 10^-scale`.
pub fn new(coefficient big.Integer, scale int) Decimal {
if scale < 0 {
panic('math.decimal: scale cannot be negative')
}
return normalize(coefficient, scale)
}
// from_int creates a decimal from an int.
pub fn from_int(value int) Decimal {
return Decimal{
coefficient_value: big.integer_from_int(value)
scale_value: 0
}
}
// from_i64 creates a decimal from an i64.
pub fn from_i64(value i64) Decimal {
return Decimal{
coefficient_value: big.integer_from_i64(value)
scale_value: 0
}
}
// from_u64 creates a decimal from a u64.
pub fn from_u64(value u64) Decimal {
return Decimal{
coefficient_value: big.integer_from_u64(value)
scale_value: 0
}
}
// from_string parses a decimal string without exponent notation.
pub fn from_string(input string) !Decimal {
value := input.trim_space()
if value.len == 0 {
return error('math.decimal: empty input')
}
mut negative := false
mut seen_dot := false
mut digit_count := 0
mut scale := 0
mut digits := strings.new_builder(value.len)
for index, ch in value {
if index == 0 && (ch == `+` || ch == `-`) {
negative = ch == `-`
continue
}
if ch >= `0` && ch <= `9` {
digits.write_u8(ch)
digit_count++
if seen_dot {
scale++
}
continue
}
if ch == `.` && !seen_dot {
seen_dot = true
continue
}
return error('math.decimal: invalid decimal value `${input}`')
}
if digit_count == 0 {
return error('math.decimal: invalid decimal value `${input}`')
}
mut coefficient_string := digits.str()
if negative {
coefficient_string = '-' + coefficient_string
}
coefficient := big.integer_from_string(coefficient_string)!
return new(coefficient, scale)
}
// coefficient returns the integer coefficient stored by `d`.
pub fn (d Decimal) coefficient() big.Integer {
return d.coefficient_value
}
// scale returns the number of decimal places stored by `d`.
pub fn (d Decimal) scale() int {
return d.scale_value
}
// is_zero returns true when `d == 0`.
pub fn (d Decimal) is_zero() bool {
return d.coefficient_value.signum == 0
}
// abs returns the absolute value of `d`.
pub fn (d Decimal) abs() Decimal {
return Decimal{
coefficient_value: d.coefficient_value.abs()
scale_value: d.scale_value
}
}
// neg returns the negated value of `d`.
pub fn (d Decimal) neg() Decimal {
return Decimal{
coefficient_value: d.coefficient_value.neg()
scale_value: d.scale_value
}
}
// str returns the canonical decimal representation of `d`.
pub fn (d Decimal) str() string {
if d.coefficient_value.signum == 0 {
return '0'
}
if d.scale_value == 0 {
return d.coefficient_value.str()
}
mut prefix := ''
if d.coefficient_value.signum < 0 {
prefix = '-'
}
absolute := d.coefficient_value.abs().str()
if d.scale_value >= absolute.len {
padding := strings.repeat(`0`, d.scale_value - absolute.len)
return '${prefix}0.${padding}${absolute}'
}
split := absolute.len - d.scale_value
return '${prefix}${absolute[..split]}.${absolute[split..]}'
}
// + returns the exact sum of `left` and `right`.
pub fn (left Decimal) + (right Decimal) Decimal {
scale := max_scale(left.scale_value, right.scale_value)
return new(left.rescaled_coefficient(scale) + right.rescaled_coefficient(scale), scale)
}
// - returns the exact difference of `left` and `right`.
pub fn (left Decimal) - (right Decimal) Decimal {
scale := max_scale(left.scale_value, right.scale_value)
return new(left.rescaled_coefficient(scale) - right.rescaled_coefficient(scale), scale)
}
// * returns the exact product of `left` and `right`.
pub fn (left Decimal) * (right Decimal) Decimal {
return new(left.coefficient_value * right.coefficient_value, left.scale_value +
right.scale_value)
}
// / divides `dividend` by `divisor` using `default_division_precision`.
pub fn (dividend Decimal) / (divisor Decimal) Decimal {
return dividend.div_prec(divisor, default_division_precision) or { panic(err) }
}
// div_prec divides `dividend` by `divisor` and rounds half up to `precision`
// digits after the decimal point.
pub fn (dividend Decimal) div_prec(divisor Decimal, precision int) !Decimal {
if precision < 0 {
return error('math.decimal: precision cannot be negative')
}
if divisor.coefficient_value.signum == 0 {
return error('math.decimal: cannot divide by zero')
}
mut quotient, remainder := scaled_div_mod(dividend, divisor, precision)
if remainder.signum != 0 {
twice_remainder := remainder * big.two_int
if !(twice_remainder < scaled_divisor(dividend, divisor)) {
quotient += big.one_int
}
}
if dividend.coefficient_value.signum * divisor.coefficient_value.signum < 0 {
quotient = quotient.neg()
}
return new(quotient, precision)
}
// == returns true when `left` and `right` represent the same value.
pub fn (left Decimal) == (right Decimal) bool {
return left.scale_value == right.scale_value
&& left.coefficient_value == right.coefficient_value
}
// < returns true when `left` is smaller than `right`.
pub fn (left Decimal) < (right Decimal) bool {
scale := max_scale(left.scale_value, right.scale_value)
return left.rescaled_coefficient(scale) < right.rescaled_coefficient(scale)
}
fn normalize(coefficient big.Integer, scale int) Decimal {
if coefficient.signum == 0 {
return zero
}
mut normalized_coefficient := coefficient
mut normalized_scale := scale
for normalized_scale > 0 {
quotient, remainder := normalized_coefficient.div_mod(big.c10)
if remainder.signum != 0 {
break
}
normalized_coefficient = quotient
normalized_scale--
}
return Decimal{
coefficient_value: normalized_coefficient
scale_value: normalized_scale
}
}
fn (d Decimal) rescaled_coefficient(scale int) big.Integer {
if scale <= d.scale_value || d.coefficient_value.signum == 0 {
return d.coefficient_value
}
return d.coefficient_value * pow10(scale - d.scale_value)
}
fn pow10(exponent int) big.Integer {
if exponent <= 0 {
return big.one_int
}
return big.c10.pow(u32(exponent))
}
fn scaled_divisor(dividend Decimal, divisor Decimal) big.Integer {
return divisor.coefficient_value.abs() * pow10(dividend.scale_value)
}
fn scaled_div_mod(dividend Decimal, divisor Decimal, precision int) (big.Integer, big.Integer) {
numerator := dividend.coefficient_value.abs() * pow10(divisor.scale_value + precision)
return numerator.div_mod(scaled_divisor(dividend, divisor))
}
fn max_scale(a int, b int) int {
if a > b {
return a
}
return b
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
import math.big
import math.decimal
fn must_decimal(value string) decimal.Decimal {
return decimal.from_string(value) or { panic(err) }
}
fn assert_parse_error(value string, expected string) {
decimal.from_string(value) or {
assert err.msg() == expected
return
}
assert false
}
fn assert_div_error(dividend string, divisor string, precision int, expected string) {
must_decimal(dividend).div_prec(must_decimal(divisor), precision) or {
assert err.msg() == expected
return
}
assert false
}
fn test_new_and_getters() {
value := decimal.new(big.integer_from_string('123400') or { panic(err) }, 4)
assert value.str() == '12.34'
assert value.coefficient().str() == '1234'
assert value.scale() == 2
}
fn test_from_ints() {
assert decimal.from_int(42).str() == '42'
assert decimal.from_i64(-99).str() == '-99'
assert decimal.from_u64(123456789).str() == '123456789'
}
fn test_from_string_normalizes_trailing_zeroes() {
assert must_decimal('00123.4500').str() == '123.45'
assert must_decimal('-0.5000').str() == '-0.5'
assert must_decimal('.25').str() == '0.25'
assert must_decimal('5.').str() == '5'
assert must_decimal('0.000').is_zero()
}
fn test_from_string_rejects_invalid_input() {
assert_parse_error('', 'math.decimal: empty input')
assert_parse_error('abc', 'math.decimal: invalid decimal value `abc`')
assert_parse_error('1.2.3', 'math.decimal: invalid decimal value `1.2.3`')
assert_parse_error('+-1', 'math.decimal: invalid decimal value `+-1`')
assert_parse_error('.', 'math.decimal: invalid decimal value `.`')
}
fn test_add_sub_and_mul() {
assert (must_decimal('1.23') + must_decimal('4.5')).str() == '5.73'
assert (must_decimal('4.5') - must_decimal('1.23')).str() == '3.27'
assert (must_decimal('12.5') * must_decimal('0.04')).str() == '0.5'
assert (must_decimal('9999999999999999999999999999.99') + must_decimal('0.01')).str() == '10000000000000000000000000000'
}
fn test_neg_abs_and_compare() {
assert must_decimal('-1.20').neg().str() == '1.2'
assert must_decimal('-1.20').abs().str() == '1.2'
assert must_decimal('1.20') == must_decimal('1.2')
assert must_decimal('-2') < must_decimal('-1.99')
assert must_decimal('0.009') < must_decimal('0.01')
}
fn test_div_prec_rounds_half_up() {
assert must_decimal('1').div_prec(must_decimal('3'), 2)!.str() == '0.33'
assert must_decimal('1').div_prec(must_decimal('6'), 2)!.str() == '0.17'
assert must_decimal('-1').div_prec(must_decimal('6'), 2)!.str() == '-0.17'
assert must_decimal('10').div_prec(must_decimal('4'), 2)!.str() == '2.5'
}
fn test_div_operator_uses_default_precision() {
assert (must_decimal('1') / must_decimal('8')).str() == '0.125'
assert (must_decimal('1') / must_decimal('3')).str() == '0.3333333333333333'
}
fn test_div_prec_errors() {
assert_div_error('1', '0', 2, 'math.decimal: cannot divide by zero')
assert_div_error('1', '2', -1, 'math.decimal: precision cannot be negative')
}