-
Notifications
You must be signed in to change notification settings - Fork 20
/
number.go
188 lines (150 loc) · 4.82 KB
/
number.go
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package types
import (
"math"
"regexp"
"strings"
"github.com/nyaruka/goflow/envs"
"github.com/nyaruka/goflow/utils"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
)
// only parse numbers like 123 or 123.456 or .456
var decimalRegexp = regexp.MustCompile(`^-?(([0-9]+)|([0-9]+\.[0-9]+)|(\.[0-9]+))$`)
func init() {
decimal.MarshalJSONWithoutQuotes = true
}
// XNumber is a whole or fractional number.
//
// @(1234) -> 1234
// @(1234.5678) -> 1234.5678
// @(format_number(1234.5670)) -> 1,234.567
// @(json(1234.5678)) -> 1234.5678
//
// @type number
type XNumber struct {
native decimal.Decimal
}
// NewXNumber creates a new XNumber
func NewXNumber(value decimal.Decimal) XNumber {
return XNumber{native: value}
}
// NewXNumberFromInt creates a new XNumber from the given int
func NewXNumberFromInt(value int) XNumber {
return NewXNumber(decimal.New(int64(value), 0))
}
// NewXNumberFromInt64 creates a new XNumber from the given int
func NewXNumberFromInt64(value int64) XNumber {
return NewXNumber(decimal.New(value, 0))
}
// RequireXNumberFromString creates a new XNumber from the given string or panics (used for tests)
func RequireXNumberFromString(value string) XNumber {
num, err := newXNumberFromString(value)
if err != nil {
panic(errors.Wrapf(err, "error parsing '%s' as number", value))
}
return num
}
// Describe returns a representation of this type for error messages
func (x XNumber) Describe() string { return x.Render() }
// Truthy determines truthiness for this type
func (x XNumber) Truthy() bool {
return !x.Equals(XNumberZero)
}
// Render returns the canonical text representation
func (x XNumber) Render() string { return x.Native().String() }
// Format returns the pretty text representation
func (x XNumber) Format(env envs.Environment) string {
return x.FormatCustom(env.NumberFormat(), -1, true)
}
// FormatCustom provides customised formatting
func (x XNumber) FormatCustom(format *envs.NumberFormat, places int, groupDigits bool) string {
var formatted string
if places >= 0 {
formatted = x.Native().StringFixed(int32(places))
} else {
formatted = x.Native().String()
}
parts := strings.Split(formatted, ".")
// add thousands separators
if groupDigits {
sb := strings.Builder{}
for i, r := range parts[0] {
sb.WriteRune(r)
d := (len(parts[0]) - 1) - i
if d%3 == 0 && d > 0 {
sb.WriteString(format.DigitGroupingSymbol)
}
}
parts[0] = sb.String()
}
return strings.Join(parts, format.DecimalSymbol)
}
// String returns the native string representation of this type
func (x XNumber) String() string { return `XNumber(` + x.Render() + `)` }
// Native returns the native value of this type
func (x XNumber) Native() decimal.Decimal { return x.native }
// Equals determines equality for this type
func (x XNumber) Equals(o XValue) bool {
other := o.(XNumber)
return x.Native().Equals(other.Native())
}
// Compare compares this number to another
func (x XNumber) Compare(o XValue) int {
other := o.(XNumber)
return x.Native().Cmp(other.Native())
}
// MarshalJSON is called when a struct containing this type is marshaled
func (x XNumber) MarshalJSON() ([]byte, error) {
return x.Native().MarshalJSON()
}
// UnmarshalJSON is called when a struct containing this type is unmarshaled
func (x *XNumber) UnmarshalJSON(data []byte) error {
nativePtr := &x.native
return nativePtr.UnmarshalJSON(data)
}
// XNumberZero is the zero number value
var XNumberZero = NewXNumber(decimal.Zero)
var _ XValue = XNumberZero
// parses a number from a string
func newXNumberFromString(s string) (XNumber, error) {
s = strings.TrimSpace(s)
if !decimalRegexp.MatchString(s) {
return XNumberZero, errors.New("not a valid number format")
}
// we can assume anything that matched our regex is parseable
d := decimal.RequireFromString(s)
return NewXNumber(d), nil
}
// ToXNumber converts the given value to a number or returns an error if that isn't possible
func ToXNumber(env envs.Environment, x XValue) (XNumber, XError) {
if !utils.IsNil(x) {
switch typed := x.(type) {
case XError:
return XNumberZero, typed
case XNumber:
return typed, nil
case XText:
parsed, err := newXNumberFromString(typed.Native())
if err == nil {
return parsed, nil
}
case *XObject:
if typed.hasDefault() {
return ToXNumber(env, typed.Default())
}
}
}
return XNumberZero, NewXErrorf("unable to convert %s to a number", Describe(x))
}
// ToInteger tries to convert the passed in value to an integer or returns an error if that isn't possible
func ToInteger(env envs.Environment, x XValue) (int, XError) {
number, err := ToXNumber(env, x)
if err != nil {
return 0, err
}
intPart := number.Native().IntPart()
if intPart < math.MinInt32 || intPart > math.MaxInt32 {
return 0, NewXErrorf("number value %s is out of range for an integer", number.Render())
}
return int(intPart), nil
}