///|
enum DecimalSign {
Positive
Negative
NaN
PosInfinity
NegInfinity
} derive(Eq, Show, Debug)
///|
struct PgDecimal {
sign : DecimalSign
weight : Int // Int16 range in practice
dscale : Int // Int16 range in practice (>= 0)
digits : Array[Int] // each entry in 0..=9999
} derive(Show, Debug)
///|
pub impl @pgclient.ToSql for PgDecimal with fn format(_, _) {
@pgclient.WireFormat::Binary
}
///|
pub impl @pgclient.ToSql for PgDecimal with fn accepts(_, type_) {
type_.oid == NUMERIC_OID
}
///|
pub impl @pgclient.ToSql for PgDecimal with fn moonbit_type_name(_) {
"PgDecimal"
}
///|
pub impl @pgclient.ToSql for PgDecimal with fn to_sql(self, _, buf) {
//(Int, Int, Int, Int, Array[Int])
let (ndigits, weight, sign_raw, dscale, digits) = match self.sign {
NaN => (0, 0, SIGN_NAN, 0, [])
PosInfinity => (0, 0, SIGN_PINF, 0, [])
NegInfinity => (0, 0, SIGN_NINF, 0, [])
Positive =>
(self.digits.length(), self.weight, SIGN_POS, self.dscale, self.digits)
Negative =>
(self.digits.length(), self.weight, SIGN_NEG, self.dscale, self.digits)
}
buf.write_int16_be(Int16::from_int(ndigits))
buf.write_int16_be(Int16::from_int(weight))
buf.write_int16_be(Int16::from_int(sign_raw))
buf.write_int16_be(Int16::from_int(dscale))
for i = 0; i < digits.length(); i = i + 1 {
buf.write_int16_be(Int16::from_int(digits[i]))
}
No
}
///|
pub impl @pgclient.FromSql for PgDecimal with fn accepts(type_) {
type_.oid == NUMERIC_OID
}
///|
pub impl @pgclient.FromSql for PgDecimal with fn moonbit_type_name() {
"PgDecimal"
}
///|
pub impl @pgclient.FromSql for PgDecimal with fn from_sql(_, format, raw) {
match format {
@pgclient.WireFormat::Binary => {
if raw.length() < 8 {
raise UnexpectedNullorInvalid(
"PgDecimal -> @Decimal.Decimal. Raw bytes - \{raw}",
)
}
let (ndigits, weight, sign_raw, dscale, digit_bytes) = match raw {
[
i16be(ndigits),
i16be(weight),
i16be(sign),
i16be(dscale),
.. digit_bytes,
] => (ndigits, weight, sign, dscale, digit_bytes)
_ =>
raise UnexpectedNullorInvalid(
"PgDecimal -> @Decimal.Decimal. Raw bytes - \{raw}",
)
}
let sign : DecimalSign = match sign_raw {
SIGN_POS => Positive
SIGN_NEG => Negative
SIGN_NAN => NaN
SIGN_PINF => PosInfinity
SIGN_NINF => NegInfinity
_other =>
raise UnexpectedNullorInvalid(
"PgDecimal -> @Decimal.Decimal. Raw bytes - \{raw}",
)
}
if sign == NaN || sign == PosInfinity || sign == NegInfinity {
return PgDecimal::{ sign, weight: 0, dscale: 0, digits: [] }
}
let digits = parse_numeric_digits(digit_bytes, ndigits, [])
return PgDecimal::{ sign, weight, dscale, digits }
}
_ =>
raise UnexpectedNullorInvalid(
"PgDecimal -> @Decimal.Decimal. Raw bytes - \{raw}",
)
}
}
///|
fn parse_numeric_digits(
bytes : BytesView,
count : Int,
acc : Array[Int],
) -> Array[Int] raise {
if count == 0 {
return acc
}
match bytes {
[i16be(digit), .. rest] =>
parse_numeric_digits(rest, count - 1, [..acc, digit])
_ => fail("Invalid numeric digit array")
}
}
///|
const SIGN_POS : Int = 0x0000
///|
const SIGN_NEG : Int = 0x4000
///|
const SIGN_NAN : Int = 0xC000
///|
const SIGN_PINF : Int = 0xD000
///|
const SIGN_NINF : Int = 0xF000
///|
pub fn PgDecimal::to_string(pg : PgDecimal) -> String {
match pg.sign {
NaN => return "NaN"
PosInfinity => return "Infinity"
NegInfinity => return "-Infinity"
_ => ()
}
if pg.digits.is_empty() {
return if pg.sign == Negative { "-0" } else { "0" }
}
let out = StringBuilder::new()
if pg.sign == Negative {
out.write_string("-")
}
let groups_before_decimal = pg.weight + 1
// Integer part
if groups_before_decimal <= 0 {
out.write_string("0")
} else {
for i = 0; i < groups_before_decimal; i = i + 1 {
let group = if i < pg.digits.length() { pg.digits[i] } else { 0 }
if i == 0 {
out.write_string(group.to_string())
} else {
out.write_string(group.to_string().pad_start(4, '0'))
}
}
}
// Fractional part
if pg.dscale > 0 {
out.write_string(".")
let frac = StringBuilder::new()
if groups_before_decimal < 0 {
for i = 0; i < -groups_before_decimal; i = i + 1 {
frac.write_string("0000")
}
for digit in pg.digits {
frac.write_string(digit.to_string().pad_start(4, '0'))
}
} else {
let start = if groups_before_decimal > pg.digits.length() {
pg.digits.length()
} else {
groups_before_decimal
}
for i = start; i < pg.digits.length(); i = i + 1 {
frac.write_string(pg.digits[i].to_string().pad_start(4, '0'))
}
}
let frac_text = frac.to_string().pad_end(pg.dscale, '0')
out.write_string(frac_text[0:pg.dscale].to_owned())
}
out.to_string()
}
///|
/// Parse a decimal string (e.g. `"123.45"`, `"-0.001"`, `"NaN"`) into a
/// `PgDecimal`.
///
fn PgDecimal::from_string(s : String) -> PgDecimal raise {
let s = s.trim()
match s {
"NaN" | "nan" => return { sign: NaN, weight: 0, dscale: 0, digits: [] }
"Infinity" | "+Infinity" | "inf" | "+inf" =>
return { sign: PosInfinity, weight: 0, dscale: 0, digits: [] }
"-Infinity" | "-inf" =>
return { sign: NegInfinity, weight: 0, dscale: 0, digits: [] }
_ => ()
}
let (sign, unsigned) = match s[0:1] {
"-" => (Negative, s[1:])
"+" => (Negative, s[1:])
_ => (Positive, s)
}
let (int_part, frac_part) = match unsigned.split(".").to_array() {
[i] => (i, "")
[i, f] => (i, f.to_owned())
_ => fail("Invalid decimal")
}
let dscale = frac_part.length()
// Integer groups (left padded)
let int_groups = if int_part == "" {
[]
} else {
decimal_to_base10000_left(int_part)
}
// Fraction groups (right padded)
let frac_groups = if frac_part == "" {
[]
} else {
decimal_to_base10000_right(frac_part)
}
let all_digits = int_groups + frac_groups
// Find first non-zero group
let mut first = 0
while first < all_digits.length() && all_digits[first] == 0 {
first = first + 1
}
if first == all_digits.length() {
return { sign: Positive, weight: 0, dscale, digits: [] }
}
// Find last non-zero group
let mut last = all_digits.length() - 1
while last >= first && all_digits[last] == 0 {
last = last - 1
}
let digits = all_digits[first:last + 1].to_owned()
let weight = int_groups.length() - first - 1
{ sign, weight, dscale, digits }
}
///|
fn decimal_to_base10000_left(s : StringView) -> Array[Int] raise {
let pad = (4 - s.length() % 4) % 4
let padded = "0".repeat(pad) + s.to_owned()
let result = []
for i = 0; i < padded.length(); i = i + 4 {
result.push(@string.parse_int(padded[i:i + 4]))
}
result
}
///|
fn decimal_to_base10000_right(s : StringView) -> Array[Int] raise {
let pad = (4 - s.length() % 4) % 4
let padded = s + "0".repeat(pad)
let result = []
for i = 0; i < padded.length(); i = i + 4 {
result.push(@string.parse_int(padded[i:i + 4]))
}
result
}
///|
pub fn PgDecimal::of_decimal(s : @decimal.Decimal) -> PgDecimal raise {
s.to_string() |> PgDecimal::from_string()
}
///|
pub fn PgDecimal::to_decimal(self : Self) -> @decimal.Decimal raise {
match @decimal.Decimal::from_string(self.to_string()) {
Some(decimal) => decimal
_ => fail("Invalid decimal string")
}
}
///|
test "pg_decimal roundtrip" {
let cases = [
"0", "1", "-1", "123.45", "-123.45", "12345678.90", "0.0012", "0.00000012", "0.000000000123",
"-0.00000012", "999999999999999999999999999999.999999999999", "10000", "100000000",
"1.0000", "0.0001", "0.0000000000000001",
]
for text in cases {
let pg = PgDecimal::from_string(text)
let roundtrip = PgDecimal::to_string(pg)
assert_eq(roundtrip, text)
}
}
///|
test "negative weight roundtrip" {
let text = "0.00000012"
let pg = PgDecimal::from_string(text)
assert_eq(pg.weight, -2)
assert_eq(pg.digits, [12])
assert_eq(PgDecimal::to_string(pg), text)
}