aboutsummaryrefslogtreecommitdiff
path: root/backend/internal/utils/money.go
blob: 2dc22861ac554e3ba819dcdabaecd260144edc53 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package utils

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/Rhymond/go-money"
)

// supported currencies
var currencies = money.Currencies{
	"USD": money.GetCurrency(money.USD),
	"EUR": money.GetCurrency(money.EUR),
	"GBP": money.GetCurrency(money.GBP),
	"JPY": money.GetCurrency(money.JPY),
	"CNY": money.GetCurrency(money.CNY),
}

func ParseMoney(s string) (*money.Money, error) {
	for _, c := range currencies {
		numPart, ok := isCurrency(s, c)
		if !ok {
			continue
		}

		// Parse the number part
		num, err := strconv.ParseUint(numPart, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("failed to parse number: %w", err)
		}

		return money.New(int64(num), c.Code), nil
	}
	return nil, fmt.Errorf("matching currency not found")
}

func isCurrency(s string, c *money.Currency) (string, bool) {
	var numPart string
	for _, tp := range c.Template {
		switch tp {
		case '$':
			// There should be a matching grapheme in the s at this position
			remaining, ok := strings.CutPrefix(s, c.Grapheme)
			if !ok {
				return "", false
			}
			s = remaining
		case '1':
			// There should be a number, thousands, or decimal separator in the s at this position
			// Number of expected decimal places
			decimalFound := -1
			// Read from string until a non-number, non-thousands, non-decimal, or EOF is found
			for len(s) > 0 && (string(s[0]) == c.Thousand || string(s[0]) == c.Decimal || '0' <= s[0] && s[0] <= '9') {
				// If the character is a number
				if '0' <= s[0] && s[0] <= '9' {
					// If we've hit decimal limit, break
					if decimalFound == 0 {
						break
					}
					// add the number to the numPart
					numPart += string(s[0])
					// Decrement the decimal count
					// If the decimal has been found, `decimalFound` is positive
					// If the decimal hasn't been found, `decimalFound` is negative, and decrementing it does nothing
					decimalFound--
				}
				// If decimal has been found (>= 0) and the character is a thousand separator or decimal separator,
				// then the number is invalid
				if decimalFound >= 0 && (string(s[0]) == c.Thousand || string(s[0]) == c.Decimal) {
					return "", false
				}
				// If the character is a decimal separator, set `decimalFound` to the number of
				// expected decimal places for the currency
				if string(s[0]) == c.Decimal {
					decimalFound = c.Fraction
				}
				// Move to the next character
				s = s[1:]
			}
			if decimalFound > 0 {
				// If there should be more decimal places, add them
				numPart += strings.Repeat("0", decimalFound)
			} else if decimalFound < 0 {
				// If no decimal was found, add the expected number of decimal places
				numPart += strings.Repeat("0", c.Fraction)
			}
		case ' ':
			// There should be a space in the s at this position
			if len(s) == 0 || s[0] != ' ' {
				return "", false
			}
			s = s[1:]
		default:
			panic(fmt.Sprintf("unsupported template character: %c", tp))
		}
	}
	return numPart, true
}