From 228b422393ff30c263c38eb27b1ead6cb10ce5f6 Mon Sep 17 00:00:00 2001 From: biondizzle Date: Mon, 16 Feb 2026 11:35:42 -0500 Subject: [PATCH] no more float conversion --- dynamodb/number.odin | 295 +++++++++++++++++++++++++++++++++++++----- dynamodb/storage.odin | 7 +- 2 files changed, 268 insertions(+), 34 deletions(-) diff --git a/dynamodb/number.odin b/dynamodb/number.odin index 96d1663..ceffb40 100644 --- a/dynamodb/number.odin +++ b/dynamodb/number.odin @@ -447,51 +447,286 @@ decode_magnitude :: proc(data: []byte, positive: bool) -> (DDB_Number, bool) { } // ============================================================================ -// Arithmetic Operations +// Decimal Arithmetic (38-digit precision, no float conversion) // ============================================================================ -// Add two DDB_Numbers +MAX_DDB_PRECISION :: 38 + +// Add two DDB_Numbers with full decimal precision. +// Returns an owned DDB_Number. add_ddb_numbers :: proc(a: DDB_Number, b: DDB_Number) -> (DDB_Number, bool) { - // Convert to f64 for arithmetic (loses precision but matches current behavior) - // TODO: Implement proper decimal arithmetic - a_f64, a_ok := ddb_number_to_f64(a) - b_f64, b_ok := ddb_number_to_f64(b) - if !a_ok || !b_ok { - return {}, false + if is_ddb_number_zero(a) { return clone_ddb_number(b), true } + if is_ddb_number_zero(b) { return clone_ddb_number(a), true } + + if a.sign == b.sign { + // Same sign: add magnitudes, keep sign + result, ok := add_magnitudes(a, b) + if !ok { return {}, false } + result.sign = a.sign + return result, true } - result := a_f64 + b_f64 - return f64_to_ddb_number(result) + // Different signs: subtract smaller magnitude from larger + cmp := compare_ddb_number_magnitudes(a, b) + if cmp == 0 { + return DDB_Number{ + sign = true, + integer_part = strings.clone("0"), + fractional_part = strings.clone(""), + exponent = 0, + }, true + } + + if cmp > 0 { + result, ok := subtract_magnitudes(a, b) + if !ok { return {}, false } + result.sign = a.sign + return result, true + } else { + result, ok := subtract_magnitudes(b, a) + if !ok { return {}, false } + result.sign = b.sign + return result, true + } } -// Subtract two DDB_Numbers +// Subtract two DDB_Numbers: a - b subtract_ddb_numbers :: proc(a: DDB_Number, b: DDB_Number) -> (DDB_Number, bool) { - // Convert to f64 for arithmetic - a_f64, a_ok := ddb_number_to_f64(a) - b_f64, b_ok := ddb_number_to_f64(b) - if !a_ok || !b_ok { + neg_b := b + neg_b.sign = !b.sign + return add_ddb_numbers(a, neg_b) +} + +// ============================================================================ +// Internal arithmetic helpers +// ============================================================================ + +// Expand a DDB_Number to effective integer and fractional digit bytes +// with the exponent fully applied. Returns heap-allocated slices (caller frees). +@(private="file") +expand_digits :: proc(num: DDB_Number) -> (int_digits: []u8, frac_digits: []u8) { + dp := len(num.integer_part) + int(num.exponent) + all_len := len(num.integer_part) + len(num.fractional_part) + + if dp <= 0 { + // Everything is fractional, need leading zeros + frac := make([]u8, -dp + all_len) + for i in 0..<(-dp) { + frac[i] = '0' + } + for i in 0..= all_len { + // Everything is integer, may need trailing zeros + int_d := make([]u8, dp) + for i in 0.. 0 { + frac = make([]u8, remaining) + for i in frac_split.. DDB_Number { + norm := normalize_ddb_number(num) + + // Clone the normalized subslices BEFORE freeing originals + new_int := strings.clone(norm.integer_part) + new_frac := strings.clone(norm.fractional_part) + + // Free the originals + delete(num.integer_part) + delete(num.fractional_part) + + return DDB_Number{ + sign = norm.sign, + integer_part = new_int, + fractional_part = new_frac, + exponent = norm.exponent, + } +} + +// Add absolute values. Returns owned DDB_Number (sign=true). +@(private="file") +add_magnitudes :: proc(a: DDB_Number, b: DDB_Number) -> (DDB_Number, bool) { + a_int, a_frac := expand_digits(a) + b_int, b_frac := expand_digits(b) + defer { delete(a_int); delete(a_frac); delete(b_int); delete(b_frac) } + + max_int := max(len(a_int), len(b_int)) + max_frac := max(len(a_frac), len(b_frac)) + total := max_int + max_frac + + // Build zero-padded aligned arrays + a_aligned := make([]u8, total) + b_aligned := make([]u8, total) + defer { delete(a_aligned); delete(b_aligned) } + + for i in 0..= 0; i -= 1 { + sum := (a_aligned[i] - '0') + (b_aligned[i] - '0') + carry + result[i + 1] = (sum % 10) + '0' + carry = sum / 10 + } + result[0] = carry + '0' + + // Split: decimal point is at max_int + 1 (carry slot shifts everything) + int_end := max_int + 1 + int_str := strings.clone(string(result[:int_end])) + frac_str := strings.clone(string(result[int_end:])) + delete(result) + + num := normalize_owned(DDB_Number{ + sign = true, + integer_part = int_str, + fractional_part = frac_str, + exponent = 0, + }) + + if len(num.integer_part) + len(num.fractional_part) > MAX_DDB_PRECISION { + delete(num.integer_part) + delete(num.fractional_part) return {}, false } - result := a_f64 - b_f64 - return f64_to_ddb_number(result) + return num, true } -// ============================================================================ -// Conversion Helpers -// ============================================================================ +// Subtract absolute values: |a| - |b|, where |a| >= |b|. +// Returns owned DDB_Number (sign=true). +@(private="file") +subtract_magnitudes :: proc(a: DDB_Number, b: DDB_Number) -> (DDB_Number, bool) { + a_int, a_frac := expand_digits(a) + b_int, b_frac := expand_digits(b) + defer { delete(a_int); delete(a_frac); delete(b_int); delete(b_frac) } -// Convert DDB_Number to f64 (may lose precision) -ddb_number_to_f64 :: proc(num: DDB_Number) -> (f64, bool) { - str := ddb_number_to_string(num) - return strconv.parse_f64(str) -} + max_int := max(len(a_int), len(b_int)) + max_frac := max(len(a_frac), len(b_frac)) + total := max_int + max_frac -// Convert f64 to DDB_Number -f64_to_ddb_number :: proc(val: f64) -> (DDB_Number, bool) { - // Format with enough precision to preserve the value - str := fmt.aprintf("%.17g", val) - return parse_ddb_number(str) + a_aligned := make([]u8, total) + b_aligned := make([]u8, total) + defer { delete(a_aligned); delete(b_aligned) } + + for i in 0..= 0; i -= 1 { + ad := a_aligned[i] - '0' + bd := (b_aligned[i] - '0') + borrow + if ad < bd { + ad += 10 + borrow = 1 + } else { + borrow = 0 + } + result[i] = (ad - bd) + '0' + } + + int_str := strings.clone(string(result[:max_int])) + frac_str := strings.clone(string(result[max_int:])) + delete(result) + + if len(int_str) == 0 { + delete(int_str) + int_str = strings.clone("0") + } + + num := normalize_owned(DDB_Number{ + sign = true, + integer_part = int_str, + fractional_part = frac_str, + exponent = 0, + }) + + if len(num.integer_part) + len(num.fractional_part) > MAX_DDB_PRECISION { + delete(num.integer_part) + delete(num.fractional_part) + return {}, false + } + + return num, true } // Format a DDB_Number for display diff --git a/dynamodb/storage.odin b/dynamodb/storage.odin index ba9d63f..3416d7e 100644 --- a/dynamodb/storage.odin +++ b/dynamodb/storage.odin @@ -1262,10 +1262,9 @@ evaluate_sort_key_condition :: proc(item: Item, skc: ^Sort_Key_Condition) -> boo } return false case .BEGINS_WITH: - // BEGINS_WITH on numbers: fall through to string comparison - item_str := format_ddb_number(item_num) - cond_str := format_ddb_number(cond_num) - return strings.has_prefix(item_str, cond_str) + // begins_with is not a valid operator for Number sort keys. + // DynamoDB rejects this at validation time. Return false. + return false } return false }