@@ -6,6 +6,7 @@ use crate::{
66 types:: Type ,
77} ;
88use byteorder:: { BigEndian , ByteOrder } ;
9+ use std:: convert:: TryFrom ;
910use std:: {
1011 io,
1112 ops:: { Add , AddAssign , Sub , SubAssign } ,
@@ -20,59 +21,115 @@ use std::{
2021///
2122/// Reading `MONEY` value in text format is not supported and will cause an error.
2223///
24+ /// ### `locale_frac_digits`
25+ /// This parameter corresponds to the number of digits after the decimal separator.
26+ ///
27+ /// This value must match what Postgres is expecting for the locale set in the database
28+ /// or else the decimal value you see on the client side will not match the `money` value
29+ /// on the server side.
30+ ///
31+ /// **For _most_ locales, this value is `2`.**
32+ ///
33+ /// If you're not sure what locale your database is set to or how many decimal digits it specifies,
34+ /// you can execute `SHOW lc_monetary;` to get the locale name, and then look it up in this list
35+ /// (you can ignore the `.utf8` prefix):
36+ /// https://lh.2xlibre.net/values/frac_digits/
37+ ///
38+ /// If that link is dead and you're on a POSIX-compliant system (Unix, FreeBSD) you can also execute:
39+ ///
40+ /// ```sh
41+ /// $ LC_MONETARY=<value returned by `SHOW lc_monetary`> locale -k frac_digits
42+ /// ```
43+ ///
44+ /// And the value you want is `N` in `frac_digits=N`. If you have shell access to the database
45+ /// server you should execute it there as available locales may differ between machines.
46+ ///
47+ /// Note that if `frac_digits` for the locale is outside the range `[0, 10]`, Postgres assumes
48+ /// it's a sentinel value and defaults to 2:
49+ /// https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/cash.c#L114-L123
50+ ///
2351/// [`MONEY`]: https://www.postgresql.org/docs/current/datatype-money.html
2452#[ derive( Debug , PartialEq , Eq , Clone , Copy ) ]
25- pub struct PgMoney ( pub i64 ) ;
53+ pub struct PgMoney (
54+ /// The raw integer value sent over the wire; for locales with `frac_digits=2` (i.e. most
55+ /// of them), this will be the value in whole cents.
56+ ///
57+ /// E.g. for `select '$123.45'::money` with a locale of `en_US` (`frac_digits=2`),
58+ /// this will be `12345`.
59+ ///
60+ /// If the currency of your locale does not have fractional units, e.g. Yen, then this will
61+ /// just be the units of the currency.
62+ ///
63+ /// See the type-level docs for an explanation of `locale_frac_units`.
64+ pub i64 ,
65+ ) ;
2666
2767impl PgMoney {
28- /// Convert the money value into a [`BigDecimal`] using the correct
29- /// precision defined in the PostgreSQL settings. The default precision is
30- /// two .
68+ /// Convert the money value into a [`BigDecimal`] using `locale_frac_digits`.
69+ ///
70+ /// See the type-level docs for an explanation of `locale_frac_digits` .
3171 ///
3272 /// [`BigDecimal`]: crate::types::BigDecimal
3373 #[ cfg( feature = "bigdecimal" ) ]
34- pub fn to_bigdecimal ( self , scale : i64 ) -> bigdecimal:: BigDecimal {
74+ pub fn to_bigdecimal ( self , locale_frac_digits : i64 ) -> bigdecimal:: BigDecimal {
3575 let digits = num_bigint:: BigInt :: from ( self . 0 ) ;
3676
37- bigdecimal:: BigDecimal :: new ( digits, scale )
77+ bigdecimal:: BigDecimal :: new ( digits, locale_frac_digits )
3878 }
3979
40- /// Convert the money value into a [`Decimal`] using the correct precision
41- /// defined in the PostgreSQL settings. The default precision is two.
80+ /// Convert the money value into a [`Decimal`] using `locale_frac_digits`.
81+ ///
82+ /// See the type-level docs for an explanation of `locale_frac_digits`.
4283 ///
4384 /// [`Decimal`]: crate::types::Decimal
4485 #[ cfg( feature = "decimal" ) ]
45- pub fn to_decimal ( self , scale : u32 ) -> rust_decimal:: Decimal {
46- rust_decimal:: Decimal :: new ( self . 0 , scale )
86+ pub fn to_decimal ( self , locale_frac_digits : u32 ) -> rust_decimal:: Decimal {
87+ rust_decimal:: Decimal :: new ( self . 0 , locale_frac_digits )
4788 }
4889
49- /// Convert a [`Decimal`] value into money using the correct precision
50- /// defined in the PostgreSQL settings. The default precision is two.
90+ /// Convert a [`Decimal`] value into money using `locale_frac_digits`.
5191 ///
52- /// Conversion may involve a loss of precision.
92+ /// See the type-level docs for an explanation of `locale_frac_digits`.
93+ ///
94+ /// Note that `Decimal` has 96 bits of precision, but `PgMoney` only has 63 plus the sign bit.
95+ /// If the value is larger than 63 bits it will be truncated.
5396 ///
5497 /// [`Decimal`]: crate::types::Decimal
5598 #[ cfg( feature = "decimal" ) ]
56- pub fn from_decimal ( decimal : rust_decimal:: Decimal , scale : u32 ) -> Self {
57- let cents = ( decimal * rust_decimal:: Decimal :: new ( 10i64 . pow ( scale) , 0 ) ) . round ( ) ;
99+ pub fn from_decimal ( mut decimal : rust_decimal:: Decimal , locale_frac_digits : u32 ) -> Self {
100+ // this is all we need to convert to our expected locale's `frac_digits`
101+ decimal. rescale ( locale_frac_digits) ;
102+
103+ /// a mask to bitwise-AND with an `i64` to zero the sign bit
104+ const SIGN_MASK : i64 = i64:: MAX ;
58105
59- let mut buf : [ u8 ; 8 ] = [ 0 ; 8 ] ;
60- buf . copy_from_slice ( & cents . serialize ( ) [ 4 .. 12 ] ) ;
106+ let is_negative = decimal . is_sign_negative ( ) ;
107+ let serialized = decimal . serialize ( ) ;
61108
62- Self ( i64:: from_le_bytes ( buf) )
109+ // interpret bytes `4..12` as an i64, ignoring the sign bit
110+ // this is where truncation occurs
111+ let value = i64:: from_le_bytes (
112+ * <& [ u8 ; 8 ] >:: try_from ( & serialized[ 4 ..12 ] )
113+ . expect ( "BUG: slice of serialized should be 8 bytes" ) ,
114+ ) & SIGN_MASK ; // zero out the sign bit
115+
116+ // negate if necessary
117+ Self ( if is_negative { -value } else { value } )
63118 }
64119
65120 /// Convert a [`BigDecimal`](crate::types::BigDecimal) value into money using the correct precision
66121 /// defined in the PostgreSQL settings. The default precision is two.
67122 #[ cfg( feature = "bigdecimal" ) ]
68123 pub fn from_bigdecimal (
69124 decimal : bigdecimal:: BigDecimal ,
70- scale : u32 ,
125+ locale_frac_digits : u32 ,
71126 ) -> Result < Self , BoxDynError > {
72127 use bigdecimal:: ToPrimitive ;
73128
74- let multiplier =
75- bigdecimal:: BigDecimal :: new ( num_bigint:: BigInt :: from ( 10i128 . pow ( scale) ) , 0 ) ;
129+ let multiplier = bigdecimal:: BigDecimal :: new (
130+ num_bigint:: BigInt :: from ( 10i128 . pow ( locale_frac_digits) ) ,
131+ 0 ,
132+ ) ;
76133
77134 let cents = decimal * multiplier;
78135
@@ -277,9 +334,25 @@ mod tests {
277334 #[ test]
278335 #[ cfg( feature = "decimal" ) ]
279336 fn conversion_from_decimal_works ( ) {
280- let dec = rust_decimal:: Decimal :: new ( 12345 , 2 ) ;
337+ assert_eq ! (
338+ PgMoney ( 12345 ) ,
339+ PgMoney :: from_decimal( rust_decimal:: Decimal :: new( 12345 , 2 ) , 2 )
340+ ) ;
341+
342+ assert_eq ! (
343+ PgMoney ( 12345 ) ,
344+ PgMoney :: from_decimal( rust_decimal:: Decimal :: new( 123450 , 3 ) , 2 )
345+ ) ;
281346
282- assert_eq ! ( PgMoney ( 12345 ) , PgMoney :: from_decimal( dec, 2 ) ) ;
347+ assert_eq ! (
348+ PgMoney ( -12345 ) ,
349+ PgMoney :: from_decimal( rust_decimal:: Decimal :: new( -123450 , 3 ) , 2 )
350+ ) ;
351+
352+ assert_eq ! (
353+ PgMoney ( -12300 ) ,
354+ PgMoney :: from_decimal( rust_decimal:: Decimal :: new( -123 , 0 ) , 2 )
355+ ) ;
283356 }
284357
285358 #[ test]
0 commit comments