money.gno
4.04 Kb · 159 lines
1// Package ledger is gnosplit's arithmetic: amounts in cents, how an expense
2// is split, and the fewest transfers that settle a group. It holds no state,
3// so a later gnosplit release and an app can reuse it as is.
4package ledger
5
6import (
7 "strconv"
8 "strings"
9)
10
11// MaxCents bounds one amount: 100 million units of a currency. With rates
12// up to MaxRate the products below stay far inside int64.
13const MaxCents int64 = 10_000_000_000
14
15// MaxRate bounds a conversion rate, in millionths: 1,000,000 units for one.
16const MaxRate int64 = 1_000_000_000_000
17
18// ParseCents reads a positive amount written "42", "42.5" or "42,50": digits,
19// then at most one "." or "," and two decimals. Thousands separators are
20// refused, since "1,234" would read as 1.234.
21func ParseCents(s string) (int64, bool) {
22 n, ok := parseDecimal(s, 2)
23 if !ok || n <= 0 || n > MaxCents {
24 return 0, false
25 }
26 return n, true
27}
28
29// ParseRate reads a positive rate with up to six decimals, in millionths.
30func ParseRate(s string) (int64, bool) {
31 n, ok := parseDecimal(s, 6)
32 if !ok || n <= 0 || n > MaxRate {
33 return 0, false
34 }
35 return n, true
36}
37
38// ParseAmount reads an amount in the group's currency ("42.50"), or a foreign
39// one with its rate ("50 USD x0.92": 50 dollars at 0.92 each). note is the
40// foreign amount as typed, cleaned up, and "" for a plain one.
41func ParseAmount(s string) (cents int64, note string, ok bool) {
42 f := strings.Fields(s)
43 switch {
44 case len(f) == 1:
45 cents, ok = ParseCents(f[0])
46 return cents, "", ok
47 case len(f) == 3 && len(f[2]) > 1 && (f[2][0] == 'x' || f[2][0] == 'X'):
48 orig, ok1 := ParseCents(f[0])
49 rate, ok2 := ParseRate(f[2][1:])
50 code := strings.ToUpper(f[1])
51 if !ok1 || !ok2 || !validCode(code) {
52 return 0, "", false
53 }
54 cents = Convert(orig, rate)
55 if cents <= 0 || cents > MaxCents {
56 return 0, "", false
57 }
58 return cents, Format(orig) + " " + code + " × " + formatRate(rate), true
59 }
60 return 0, "", false
61}
62
63// Convert turns cents into cents of another currency at rate (millionths),
64// rounding half up. It splits the rate so no product overflows.
65func Convert(cents, rate int64) int64 {
66 q, r := rate/1_000_000, rate%1_000_000
67 return cents*q + (cents*r+500_000)/1_000_000
68}
69
70// Format writes cents as "1,234.56", "-12.00" or "0.00".
71func Format(cents int64) string {
72 sign := ""
73 if cents < 0 {
74 sign, cents = "-", -cents
75 }
76 units := strconv.FormatInt(cents/100, 10)
77 for i := len(units) - 3; i > 0; i -= 3 {
78 units = units[:i] + "," + units[i:]
79 }
80 frac := cents % 100
81 if frac < 10 {
82 return sign + units + ".0" + strconv.FormatInt(frac, 10)
83 }
84 return sign + units + "." + strconv.FormatInt(frac, 10)
85}
86
87// Signed is Format with a "+" on positive amounts, for balances.
88func Signed(cents int64) string {
89 if cents > 0 {
90 return "+" + Format(cents)
91 }
92 return Format(cents)
93}
94
95func parseDecimal(s string, places int) (int64, bool) {
96 s = strings.TrimSpace(s)
97 if len(s) == 0 || len(s) > 24 {
98 return 0, false
99 }
100 var n int64
101 digits, frac := 0, -1 // frac counts decimals once the separator is seen
102 for i := 0; i < len(s); i++ {
103 c := s[i]
104 switch {
105 case c >= '0' && c <= '9':
106 if frac >= 0 {
107 if frac == places {
108 return 0, false
109 }
110 frac++
111 }
112 digits++
113 n = n*10 + int64(c-'0')
114 if n > 1_000_000_000_000_000 {
115 return 0, false
116 }
117 case (c == '.' || c == ',') && frac < 0 && digits > 0:
118 frac = 0
119 default:
120 return 0, false
121 }
122 }
123 if frac == 0 { // "12." has a separator and no decimal
124 return 0, false
125 }
126 if frac < 0 {
127 frac = 0
128 }
129 for ; frac < places; frac++ {
130 if n > 1_000_000_000_000_000 {
131 return 0, false
132 }
133 n *= 10
134 }
135 return n, true
136}
137
138func validCode(c string) bool {
139 if len(c) < 1 || len(c) > 5 {
140 return false
141 }
142 for i := 0; i < len(c); i++ {
143 if c[i] < 'A' || c[i] > 'Z' {
144 return false
145 }
146 }
147 return true
148}
149
150// formatRate writes millionths without trailing zeros: 920000 is "0.92".
151func formatRate(r int64) string {
152 s := strconv.FormatInt(r/1_000_000, 10)
153 frac := strconv.FormatInt(1_000_000+r%1_000_000, 10)[1:]
154 frac = strings.TrimRight(frac, "0")
155 if frac == "" {
156 return s
157 }
158 return s + "." + frac
159}