
Decimal.pudu
Pudu42 lines2.0 KB
1/** @Validation.Rules.Decimal.Module — checks exact decimal shape */2module PuduLangValidator.Rules.Decimal34import Std.Text as Text5import PuduLangValidator.Rule as Rule67/** @Validation.Rules.Decimal.Shape — counts displayed decimal digits */8type Shape = { precision: Int, scale: Int, wholeDigits: Int }91011export fn precisionScale[T](12 builder: Rule.RuleBuilder[T, Decimal], precision: Int, scale: Int, ignoreTrailingZeros: Bool13) -> Rule.RuleBuilder[T, Decimal] {14 let checked = Rule.withCode(Rule.withMessage(Rule.mustWithArguments(builder,15 fn(_root: T, value: Decimal) -> Option[Array[(Str, Str)]] {16 let actual = shape(value, ignoreTrailingZeros)17 if precision >= 1 && scale >= 0 && scale <= precision &&18 actual.precision <= precision && actual.scale <= scale &&19 actual.wholeDigits <= precision - scale {20 None21 } else {22 Some([("Digits", show(actual.precision)), ("ActualScale", show(actual.scale))])23 }24 }), "Decimal must fit \{ExpectedPrecision\} digits with at most \{ExpectedScale\} after the point."), "PrecisionScale")25 Rule.withArgument(Rule.withArgument(checked, "ExpectedPrecision", show(precision)),26 "ExpectedScale", show(scale))27}282930fn shape(value: Decimal, ignoreTrailingZeros: Bool) -> Shape {31 let rendered = value.toText()32 let unsigned = if rendered.startsWith("-") { Text.drop(rendered, 1) } else { rendered }33 let parts = unsigned.split(".")34 let whole = Text.dropWhile(parts[0], |character: Char| character == '0')35 let fractional = if parts.length() > 1 { parts[1] } else { "" }36 let kept = if ignoreTrailingZeros {37 Text.dropWhileEnd(fractional, |character: Char| character == '0')38 } else { fractional }39 let digits = whole.length() + kept.length()40 Shape{precision: if digits == 0 { 1 } else { digits }, scale: kept.length(), wholeDigits: whole.length()}41}42