github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/pkg/money/money.go (about) 1 package money 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 ) 8 9 const precision = 100 10 11 // Money is an amount with precision 2 12 // 13 // - FromInt(15) -> 15 14 // - FromFloat(15.07) -> 15.07 15 // - FromFloat(-15.073) -> -15.07 16 // - FromFloat(15.078) -> 15.07 17 // 18 type Money int64 19 20 // FromInt converts int64 to Money 21 func FromInt(m int64) Money { 22 return Money(m * precision) 23 } 24 25 // FromFloat converts float64 to Money 26 func FromFloat(m float64) Money { 27 // We can't convert float64 to int64 with precision 2 by multiplying it by 100 because we can get something 28 // like this: 17.83 * 100 = 1782.9999999999998 29 // 30 // We can use package 'github.com/shopspring/decimal', but it requires major refactoring. 31 // So, as a temporary solution, we use this algorithm: 32 // 33 // 1. Convert float64 to string with fixed precision 34 // 2. Remove decimal separator '.' 35 // 3. Parse this strings as int64 36 // 37 38 // Use precision 3 instead of 2 because 'AppendFloat' rounds float64 39 s := strconv.AppendFloat(nil, m, 'f', 3, 64) 40 // Replace decimal separator with the first digit and first digit with the second one: 17.830 -> 178830 -> 178330 41 s[len(s)-4], s[len(s)-3] = s[len(s)-3], s[len(s)-2] 42 // Trim last 2 digits 43 s = s[:len(s)-2] 44 45 res, err := strconv.ParseInt(string(s), 10, 64) 46 if err != nil { 47 // Just in case 48 panic(err) 49 } 50 51 return Money(res) 52 } 53 54 // Int converts Money to int64 55 func (m Money) Int() int64 { 56 return int64(m) / precision 57 } 58 59 // Float converts Money to float64 60 func (m Money) Float() float64 { 61 return float64(m) / precision 62 } 63 64 // String converts Money to string. Money is always formatted as a number with 2 digits 65 // after decimal point (123.45, 123.00 and etc.) 66 func (m Money) String() string { 67 return fmt.Sprintf("%.2f", m.Float()) 68 } 69 70 // Arithmetic operations 71 72 // Add returns sum of original and passed Moneys 73 func (m Money) Add(add Money) Money { 74 return m + add 75 } 76 77 // Sub returns remainder after subtraction 78 func (m Money) Sub(sub Money) Money { 79 return m - sub 80 } 81 82 // Div divides Money by n (if n <= 0, it panics) 83 func (m Money) Div(n int64) Money { 84 if n <= 0 { 85 panic("n must be greater than zero") 86 } 87 88 // Don't use Money.ToInt for better precision 89 money := int64(m) 90 return Money(money / n) 91 } 92 93 // Other 94 95 // Round is like 'math.Round' 96 func (m Money) Round() Money { 97 if m == 0 { 98 return m 99 } 100 101 mod := m % precision 102 if mod == 0 { 103 return m 104 } 105 106 m -= mod 107 switch { 108 case mod >= 50: 109 m += precision 110 case mod <= -50: 111 m -= precision 112 } 113 114 return m 115 } 116 117 // Ceil is like 'math.Ceil' 118 func (m Money) Ceil() Money { 119 if m == 0 { 120 return m 121 } 122 123 mod := m % precision 124 if mod == 0 { 125 return m 126 } 127 128 m -= mod 129 if m > 0 { 130 m += precision 131 } 132 return m 133 } 134 135 // Floor is like 'math.Floor' 136 func (m Money) Floor() Money { 137 if m == 0 { 138 return m 139 } 140 141 mod := m % precision 142 if mod == 0 { 143 return m 144 } 145 146 m -= mod 147 if m < 0 { 148 m -= precision 149 } 150 return m 151 } 152 153 // Encoding and Decoding 154 155 var ( 156 _ json.Marshaler = (*Money)(nil) 157 _ json.Unmarshaler = (*Money)(nil) 158 ) 159 160 func (m Money) MarshalJSON() ([]byte, error) { 161 // Always format with 2 digits after decimal point (123.45, 123.00 and etc.) 162 return []byte(m.String()), nil 163 } 164 165 func (m *Money) UnmarshalJSON(data []byte) error { 166 f, err := strconv.ParseFloat(string(data), 64) 167 if err != nil { 168 return err 169 } 170 *m = FromFloat(f) 171 return nil 172 } 173 174 var _ fmt.Formatter = (*Money)(nil) 175 176 // Format implements 'fmt.Formatter' interface. It divides a number in groups of three didits 177 // separated by thin space 178 func (m Money) Format(f fmt.State, c rune) { 179 const thinSpace = " " 180 181 str := m.String() 182 183 switch c { 184 case 'd': 185 str = str[:len(str)-3] 186 if str == "-0" { 187 str = "0" 188 } 189 case 'f': 190 // Do nothing 191 default: 192 var negative bool 193 // There's a case when minus is separated by thin space (- 100 000.00). 194 // So, trim it for a while. 195 if str[0] == '-' { 196 negative = true 197 str = str[1:] 198 } 199 200 // This algorithm can be buggy because the string is changing in process, but 201 // it works for 1000000000000.00 (one trillion must be enough for all cases) and 202 // it is very simple. So, leave it as is. 203 204 for i := len(str) - 6; i > 0; i -= 3 { 205 // We don't use comma as a separator because: 206 // 207 // The 22nd General Conference on Weights and Measures declared in 2003 that 208 // "the symbol for the decimal marker shall be either the point on the line or 209 // the comma on the line". It further reaffirmed that "numbers may be divided in 210 // groups of three in order to facilitate reading; neither dots nor commas are ever 211 // inserted in the spaces between groups" 212 // 213 // Source: https://en.wikipedia.org/wiki/Decimal_separator#Current_standards 214 // 215 // Use thin space ' ' instead (https://en.wikipedia.org/wiki/Thin_space) 216 217 str = str[:i] + thinSpace + str[i:] 218 } 219 220 if negative { 221 str = "-" + str 222 } 223 } 224 225 f.Write([]byte(str)) 226 }