Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}