@@ -6,6 +6,7 @@ use crate::{
6
6
types:: Type ,
7
7
} ;
8
8
use byteorder:: { BigEndian , ByteOrder } ;
9
+ use std:: convert:: TryFrom ;
9
10
use std:: {
10
11
io,
11
12
ops:: { Add , AddAssign , Sub , SubAssign } ,
@@ -20,59 +21,115 @@ use std::{
20
21
///
21
22
/// Reading `MONEY` value in text format is not supported and will cause an error.
22
23
///
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
+ ///
23
51
/// [`MONEY`]: https://www.postgresql.org/docs/current/datatype-money.html
24
52
#[ 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
+ ) ;
26
66
27
67
impl 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` .
31
71
///
32
72
/// [`BigDecimal`]: crate::types::BigDecimal
33
73
#[ 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 {
35
75
let digits = num_bigint:: BigInt :: from ( self . 0 ) ;
36
76
37
- bigdecimal:: BigDecimal :: new ( digits, scale )
77
+ bigdecimal:: BigDecimal :: new ( digits, locale_frac_digits )
38
78
}
39
79
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`.
42
83
///
43
84
/// [`Decimal`]: crate::types::Decimal
44
85
#[ 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 )
47
88
}
48
89
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`.
51
91
///
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.
53
96
///
54
97
/// [`Decimal`]: crate::types::Decimal
55
98
#[ 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 ;
58
105
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 ( ) ;
61
108
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 } )
63
118
}
64
119
65
120
/// Convert a [`BigDecimal`](crate::types::BigDecimal) value into money using the correct precision
66
121
/// defined in the PostgreSQL settings. The default precision is two.
67
122
#[ cfg( feature = "bigdecimal" ) ]
68
123
pub fn from_bigdecimal (
69
124
decimal : bigdecimal:: BigDecimal ,
70
- scale : u32 ,
125
+ locale_frac_digits : u32 ,
71
126
) -> Result < Self , BoxDynError > {
72
127
use bigdecimal:: ToPrimitive ;
73
128
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
+ ) ;
76
133
77
134
let cents = decimal * multiplier;
78
135
@@ -277,9 +334,25 @@ mod tests {
277
334
#[ test]
278
335
#[ cfg( feature = "decimal" ) ]
279
336
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
+ ) ;
281
346
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
+ ) ;
283
356
}
284
357
285
358
#[ test]
0 commit comments