github.com/liquid-dev/text@v0.3.3-liquid/currency/format.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package currency 6 7 import ( 8 "fmt" 9 "io" 10 "sort" 11 12 "github.com/liquid-dev/text/internal/format" 13 "github.com/liquid-dev/text/internal/language/compact" 14 ) 15 16 // Amount is an amount-currency unit pair. 17 type Amount struct { 18 amount interface{} // Change to decimal(64|128). 19 currency Unit 20 } 21 22 // Currency reports the currency unit of this amount. 23 func (a Amount) Currency() Unit { return a.currency } 24 25 // TODO: based on decimal type, but may make sense to customize a bit. 26 // func (a Amount) Decimal() 27 // func (a Amount) Int() (int64, error) 28 // func (a Amount) Fraction() (int64, error) 29 // func (a Amount) Rat() *big.Rat 30 // func (a Amount) Float() (float64, error) 31 // func (a Amount) Scale() uint 32 // func (a Amount) Precision() uint 33 // func (a Amount) Sign() int 34 // 35 // Add/Sub/Div/Mul/Round. 36 37 var space = []byte(" ") 38 39 // Format implements fmt.Formatter. It accepts format.State for 40 // language-specific rendering. 41 func (a Amount) Format(s fmt.State, verb rune) { 42 v := formattedValue{ 43 currency: a.currency, 44 amount: a.amount, 45 format: defaultFormat, 46 } 47 v.Format(s, verb) 48 } 49 50 // formattedValue is currency amount or unit that implements language-sensitive 51 // formatting. 52 type formattedValue struct { 53 currency Unit 54 amount interface{} // Amount, Unit, or number. 55 format *options 56 } 57 58 // Format implements fmt.Formatter. It accepts format.State for 59 // language-specific rendering. 60 func (v formattedValue) Format(s fmt.State, verb rune) { 61 var lang compact.ID 62 if state, ok := s.(format.State); ok { 63 lang, _ = compact.RegionalID(compact.Tag(state.Language())) 64 } 65 66 // Get the options. Use DefaultFormat if not present. 67 opt := v.format 68 if opt == nil { 69 opt = defaultFormat 70 } 71 cur := v.currency 72 if cur.index == 0 { 73 cur = opt.currency 74 } 75 76 // TODO: use pattern. 77 io.WriteString(s, opt.symbol(lang, cur)) 78 if v.amount != nil { 79 s.Write(space) 80 81 // TODO: apply currency-specific rounding 82 scale, _ := opt.kind.Rounding(cur) 83 if _, ok := s.Precision(); !ok { 84 fmt.Fprintf(s, "%.*f", scale, v.amount) 85 } else { 86 fmt.Fprint(s, v.amount) 87 } 88 } 89 } 90 91 // Formatter decorates a given number, Unit or Amount with formatting options. 92 type Formatter func(amount interface{}) formattedValue 93 94 // func (f Formatter) Options(opts ...Option) Formatter 95 96 // TODO: call this a Formatter or FormatFunc? 97 98 var dummy = USD.Amount(0) 99 100 // adjust creates a new Formatter based on the adjustments of fn on f. 101 func (f Formatter) adjust(fn func(*options)) Formatter { 102 var o options = *(f(dummy).format) 103 fn(&o) 104 return o.format 105 } 106 107 // Default creates a new Formatter that defaults to currency unit c if a numeric 108 // value is passed that is not associated with a currency. 109 func (f Formatter) Default(currency Unit) Formatter { 110 return f.adjust(func(o *options) { o.currency = currency }) 111 } 112 113 // Kind sets the kind of the underlying currency unit. 114 func (f Formatter) Kind(k Kind) Formatter { 115 return f.adjust(func(o *options) { o.kind = k }) 116 } 117 118 var defaultFormat *options = ISO(dummy).format 119 120 var ( 121 // Uses Narrow symbols. Overrides Symbol, if present. 122 NarrowSymbol Formatter = Formatter(formNarrow) 123 124 // Use Symbols instead of ISO codes, when available. 125 Symbol Formatter = Formatter(formSymbol) 126 127 // Use ISO code as symbol. 128 ISO Formatter = Formatter(formISO) 129 130 // TODO: 131 // // Use full name as symbol. 132 // Name Formatter 133 ) 134 135 // options configures rendering and rounding options for an Amount. 136 type options struct { 137 currency Unit 138 kind Kind 139 140 symbol func(compactIndex compact.ID, c Unit) string 141 } 142 143 func (o *options) format(amount interface{}) formattedValue { 144 v := formattedValue{format: o} 145 switch x := amount.(type) { 146 case Amount: 147 v.amount = x.amount 148 v.currency = x.currency 149 case *Amount: 150 v.amount = x.amount 151 v.currency = x.currency 152 case Unit: 153 v.currency = x 154 case *Unit: 155 v.currency = *x 156 default: 157 if o.currency.index == 0 { 158 panic("cannot format number without a currency being set") 159 } 160 // TODO: Must be a number. 161 v.amount = x 162 v.currency = o.currency 163 } 164 return v 165 } 166 167 var ( 168 optISO = options{symbol: lookupISO} 169 optSymbol = options{symbol: lookupSymbol} 170 optNarrow = options{symbol: lookupNarrow} 171 ) 172 173 // These need to be functions, rather than curried methods, as curried methods 174 // are evaluated at init time, causing tables to be included unconditionally. 175 func formISO(x interface{}) formattedValue { return optISO.format(x) } 176 func formSymbol(x interface{}) formattedValue { return optSymbol.format(x) } 177 func formNarrow(x interface{}) formattedValue { return optNarrow.format(x) } 178 179 func lookupISO(x compact.ID, c Unit) string { return c.String() } 180 func lookupSymbol(x compact.ID, c Unit) string { return normalSymbol.lookup(x, c) } 181 func lookupNarrow(x compact.ID, c Unit) string { return narrowSymbol.lookup(x, c) } 182 183 type symbolIndex struct { 184 index []uint16 // position corresponds with compact index of language. 185 data []curToIndex 186 } 187 188 var ( 189 normalSymbol = symbolIndex{normalLangIndex, normalSymIndex} 190 narrowSymbol = symbolIndex{narrowLangIndex, narrowSymIndex} 191 ) 192 193 func (x *symbolIndex) lookup(lang compact.ID, c Unit) string { 194 for { 195 index := x.data[x.index[lang]:x.index[lang+1]] 196 i := sort.Search(len(index), func(i int) bool { 197 return index[i].cur >= c.index 198 }) 199 if i < len(index) && index[i].cur == c.index { 200 x := index[i].idx 201 start := x + 1 202 end := start + uint16(symbols[x]) 203 if start == end { 204 return c.String() 205 } 206 return symbols[start:end] 207 } 208 if lang == 0 { 209 break 210 } 211 lang = lang.Parent() 212 } 213 return c.String() 214 }