numrepr 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ #
2
+ # numrepr/format.rb -- Format numbers
3
+ #
4
+
5
+ require "supplement"
6
+
7
+
8
+ module NumRepr
9
+
10
+ class Parser
11
+
12
+ GRP = ",.'\u{2019}\u{202f}"
13
+ DEC = ".,\u{b7}"
14
+
15
+ class <<self
16
+
17
+ def into_i str
18
+ check_string str
19
+ Integer remove_grp str
20
+ rescue ArgumentError, NoMethodError
21
+ raise ArgumentError, "Couldn't parse as an integer: #{str}"
22
+ end
23
+
24
+ def into_f str
25
+ digs = prepare_float str
26
+ Float digs
27
+ rescue ArgumentError, NoMethodError
28
+ raise ArgumentError, "Couldn't parse as a float: #{str}"
29
+ end
30
+
31
+ def into_d str
32
+ digs = prepare_float str
33
+ BigDecimal digs
34
+ rescue ArgumentError, NoMethodError
35
+ raise ArgumentError, "Couldn't parse as a decimal: #{str}"
36
+ end
37
+
38
+ private
39
+
40
+ def prepare_float str
41
+ check_string str
42
+ digs, exp = str.split /e/i
43
+ ints, frac = decsplit digs
44
+ digs = remove_grps [ints, frac]
45
+ digs << "e" << exp if exp
46
+ digs
47
+ end
48
+
49
+ def check_string str
50
+ String === str or raise "Not a string: #{str.inspect}"
51
+ end
52
+
53
+ def remove_grp str
54
+ str = str.dup
55
+ self::GRP.each_char { |g| str.gsub! g, "_" }
56
+ str
57
+ end
58
+
59
+ def remove_grps strs
60
+ strs.map { |s| remove_grp s }.join "."
61
+ end
62
+
63
+ def decsplit digs
64
+ ps = []
65
+ self::DEC.each_char { |dec|
66
+ if (digs.count dec) == 1 then
67
+ p = digs.rindex dec
68
+ end
69
+ ps.push p
70
+ }
71
+ m = nil
72
+ ps.each { |p|
73
+ p or next
74
+ if not m or p > m then
75
+ m = p
76
+ end
77
+ }
78
+ if m then
79
+ ints = digs.slice! 0, m
80
+ digs.slice! 0, 1
81
+ else
82
+ ints = digs.slice! /.*\d/
83
+ end
84
+ [ints, digs]
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+
92
+ class Format
93
+
94
+ NUMDIGITS = nil # minimum number of digits in integer part (builds leading zeroes)
95
+ POS = false # plus sign if positive (still none for 0)
96
+ GRP = false # group three digits
97
+ CHGRP = "," # group character
98
+ WIDTH = nil # minimum length of integer part (builds leading spaces)
99
+
100
+ class <<self
101
+
102
+ def method_missing sym, *args, **kwargs
103
+ if sym =~ /\Abuild_/ then
104
+ (send :"from_#$'", *args, **kwargs).to_s
105
+ else
106
+ super
107
+ end
108
+ end
109
+
110
+ def from_i n
111
+ ints, = n.make_digits
112
+ i = new ints
113
+ i.do_int n.sign, true
114
+ i
115
+ end
116
+
117
+ end
118
+
119
+ def initialize chars
120
+ @chars = chars
121
+ end
122
+
123
+ def to_s ; @chars ; end
124
+
125
+
126
+ alias cls class
127
+
128
+ # Please load the "supplement" gem.
129
+ def notempty? ; @chars.notempty? rescue @chars.emtpy? ? nil : @chars ; end
130
+
131
+ def reverse! ; @chars.reverse! ; end
132
+
133
+ def pad0 n ; pad n, "0" ; end
134
+ def pad_ n ; pad n, " " if n ; end
135
+ def pad n, chr
136
+ p = n - @chars.length
137
+ @chars << chr * p if p > 0
138
+ end
139
+
140
+ def group
141
+ if cls::GRP then
142
+ l, e = 3, ""
143
+ loop do
144
+ e << (@chars.slice! 0, l)
145
+ break if @chars.empty?
146
+ e << cls::CHGRP
147
+ end
148
+ @chars.replace e
149
+ end
150
+ end
151
+
152
+ def do_sign n
153
+ if n.negative? then
154
+ @chars << "-"
155
+ elsif n.positive? then
156
+ @chars << "+" if cls::POS
157
+ end
158
+ end
159
+
160
+ def do_int nsig, finite
161
+ if finite then
162
+ pad0 cls::NUMDIGITS||1
163
+ group
164
+ end
165
+ do_sign nsig
166
+ pad_ cls::WIDTH
167
+ reverse!
168
+ end
169
+
170
+
171
+ def do_decimal frac
172
+ sep = cls.new ""
173
+ frc = cls.new frac
174
+ yield sep, frc
175
+ @chars << sep.to_s << frc.to_s
176
+ end
177
+
178
+
179
+ DEC = false # force decimal character
180
+ CHDEC = "." # character before decimal places
181
+ NUMPLACES = nil # minimum number of places (builds trailing zeroes)
182
+ ZSIGN = false # respect zero sign (determinable only in BigDecimal)
183
+ RWIDTH = nil # minimum width to the right (builds trailing spaces)
184
+
185
+ class <<self
186
+
187
+ def from_d d
188
+ ints, frac = d.make_digits
189
+ i = new ints
190
+ i.do_bd frac, (self::ZSIGN ? d.sign : d)
191
+ i
192
+ end
193
+ alias from_f from_d
194
+
195
+ end
196
+
197
+ def sep needs
198
+ @chars << cls::CHDEC if needs or cls::DEC
199
+ end
200
+
201
+ def do_bd frac, nsig
202
+ do_int nsig, frac
203
+ do_decimal frac||"" do |s,f|
204
+ if frac then
205
+ f.pad0 cls::NUMPLACES||0
206
+ f.group
207
+ s.sep f.notempty?
208
+ end
209
+ f.pad_ cls::RWIDTH
210
+ s.pad_ cls::RWIDTH&&1
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ end
217
+
218
+
219
+
220
+ class Numeric
221
+
222
+ def split
223
+ _, r = (coerce 0.0)
224
+ r.split
225
+ end
226
+
227
+ def sign ; self ; end
228
+
229
+ def make_digits
230
+ case
231
+ when nan? then "NaN".reverse
232
+ when infinite? then "Inf".reverse
233
+ when zero? then [ "", ""]
234
+ else
235
+ _, digits, _, exp = *split
236
+ f = exp - digits.length
237
+ if f >= 0 then
238
+ digits << "0" * f
239
+ digits.reverse!
240
+ [ digits, ""]
241
+ else
242
+ if exp >= 0 then
243
+ i = digits.slice! 0, exp
244
+ i.reverse!
245
+ else
246
+ i = ""
247
+ digits.prepend "0" * -exp
248
+ end
249
+ [ i, digits]
250
+ end
251
+ end
252
+ end
253
+
254
+ end
255
+
256
+
257
+ class Integer < Numeric
258
+
259
+ def make_digits
260
+ if zero? then
261
+ ""
262
+ else
263
+ abs.to_s.reverse
264
+ end
265
+ end or begin # If written in C, this one would be faster.
266
+ r = ""
267
+ n = abs
268
+ while n.nonzero? do
269
+ n, d = n.divmod 10
270
+ r << (d + "0".ord).chr
271
+ end
272
+ r
273
+ end
274
+
275
+ end
276
+
277
+
278
+ class Float < Numeric
279
+
280
+ # There seems to be no legal way to retrieve the negative sign of -0.0.
281
+ # If you really need it, convert the number to BigDecimal.
282
+ #
283
+ # -0.0.to_f.to_d.sign #=> -1
284
+ def sign ; super ; end
285
+
286
+ # analogous to BigDecimal#split
287
+ def split
288
+ m, e = abs.to_s.split /e/i
289
+ e = e.to_i
290
+ d, f = m.split "."
291
+ d.lstrip! "0" rescue d.slice! /\A0+/ # before Ruby 4.0
292
+ e += d.count "0-9"
293
+ if f then
294
+ f.rstrip! "0" rescue f.slice! /0+\z/ # before Ruby 4.0
295
+ d << f
296
+ end
297
+ [ (negative? ? -1 : 1), d, 10, e]
298
+ end
299
+
300
+ end
301
+
@@ -0,0 +1,337 @@
1
+ #
2
+ # numrepr/money.rb -- Money amount values
3
+ #
4
+
5
+ require "numrepr/format"
6
+
7
+
8
+ module NumRepr
9
+
10
+ class Money
11
+
12
+ attr_reader :cents
13
+ alias cent cents
14
+
15
+ PLACES = 2
16
+ CENTS = 10 ** PLACES
17
+
18
+ class <<self
19
+
20
+ alias parse new
21
+
22
+ def zero ; new nil ; end
23
+
24
+ def as obj
25
+ case obj
26
+ when self then obj
27
+ else new obj
28
+ end
29
+ end
30
+
31
+ def norm cents
32
+ case cents
33
+ when Integer then cents
34
+ else cents.round.to_i
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+
41
+ def initialize val = nil
42
+ @cents =
43
+ case val
44
+ when String then parse_str val
45
+ when Money then val.cents
46
+ when nil then 0
47
+ else Money.norm CENTS * val
48
+ end
49
+ rescue ArgumentError, TypeError
50
+ raise ArgumentError, "Not a Money value: \"#{val}\"."
51
+ end
52
+
53
+ private
54
+
55
+ def parse_str val
56
+ v = val.dup
57
+ v.slice! /(?:\b[A-Z]{3}\b|[$\u{a2}-\u{a5}\u{20a0}-\u{20cf}])/ # unit
58
+ v.strip!
59
+ n = v.slice! /\A\s*-/
60
+ f = v.slice! /\..*/
61
+ v.gsub! ",", "_" # PostgreSQL's money type actually returns this.
62
+ r = v.empty? ? 0 : (Integer v) * CENTS
63
+ r += ((Float "0#{f}0") * CENTS).round if f
64
+ r *= -1 if n
65
+ r
66
+ end
67
+
68
+ public
69
+
70
+ def eql? oth ; oth.class == self.class and oth.cents == @cents ; end
71
+
72
+ def hash ; @cents.hash ; end
73
+
74
+ public
75
+
76
+ def to_money ; self ; end
77
+
78
+ def to_f ; @cents/CENTS.to_f ; end
79
+ def to_i ; to_f.round ; end
80
+ def to_r ; Rational @cents, CENTS ; end
81
+ def to_d ; (BigDecimal @cents) / CENTS ; end
82
+
83
+ def succ! ; @cents = @cents.succ ; self ; end
84
+ def pred! ; @cents = @cents.pred ; self ; end
85
+ def succ ; dup.succ! ; end
86
+ def pred ; dup.pred! ; end
87
+
88
+ def zero? ; @cents.zero? ; end
89
+ def nonzero? ; @cents.nonzero? and self ; end
90
+
91
+ def sign ; @cents ; end
92
+ def positive? ; @cents.positive? ; end
93
+ def negative? ; @cents.negative? ; end
94
+ alias pos? positive?
95
+ alias neg? negative?
96
+
97
+ def add! oth ; @cents += (self.class.as oth).cents ; self ; end
98
+ def sub! oth ; @cents -= (self.class.as oth).cents ; self ; end
99
+ def add oth ; dup.add! oth ; end
100
+ def sub oth ; dup.sub! oth ; end
101
+
102
+ def mul! oth
103
+ c = case oth
104
+ when Money then
105
+ raise ArgumentError, "Currencies cannot be multiplied together"
106
+ when Integer then
107
+ Rational @cents * oth
108
+ else
109
+ @cents * oth
110
+ end
111
+ @cents = Money.norm c
112
+ self
113
+ end
114
+
115
+ def mul oth ; dup.mul! oth ; end
116
+
117
+ def div! oth
118
+ c = case oth
119
+ when Money then
120
+ raise ArgumentError, "Currency cannot be reduced by another one"
121
+ when Integer, Rational then
122
+ Rational @cents, oth
123
+ else
124
+ @cents / oth
125
+ end
126
+ @cents = Money.norm c
127
+ self
128
+ end
129
+
130
+ def div oth
131
+ case oth
132
+ when Money then Rational @cents, oth.cents
133
+ else dup.div! oth
134
+ end
135
+ end
136
+
137
+ alias + add
138
+ alias - sub
139
+ alias * mul
140
+ alias / div
141
+
142
+ def neg! ; @cents = -@cents ; self ; end
143
+ def neg ; dup.neg! ; end
144
+ def +@ ; self ; end
145
+ def -@ ; neg ; end
146
+
147
+ def abs ; @cents >= 0 ? self : neg ; end
148
+
149
+ def <=> oth
150
+ case oth
151
+ when Money then @cents <=> oth.cents
152
+ when Numeric then @cents <=> oth*CENTS
153
+ when nil then @cents.zero? ? 0 : 1
154
+ else to_r <=> oth
155
+ end
156
+ end
157
+ include Comparable
158
+
159
+ def coerce oth ; [ oth, to_r] ; end
160
+
161
+
162
+ def make_digits
163
+ r = @cents.make_digits
164
+ l = 2 - r.length
165
+ r << "0" * l if l > 0
166
+ f = r.slice! 0, 2
167
+ f.reverse!
168
+ [r, f]
169
+ end
170
+
171
+ # :call-seq:
172
+ # cur.raw -> str
173
+ #
174
+ # Build a simple string representation without spaces, group characters,
175
+ # parentheses etc. that can be understood by the classes ::parse method
176
+ # and, hopefully, by other programming languages (Some SQL, for example).
177
+ #
178
+ # c = Money.parse "$1,234.00"
179
+ # c.raw #=> "$1234.00"
180
+ #
181
+
182
+ def raw
183
+ b, c = make_digits
184
+ b << "0" if b.empty?
185
+ b.reverse!
186
+ r = "$"
187
+ r << "-" if negative?
188
+ r << b << "." << c
189
+ end
190
+ alias to_s raw
191
+ alias inspect raw
192
+
193
+ end
194
+
195
+ class Parser
196
+
197
+ DASH = "-\u{2013}"
198
+ IDASH = DASH
199
+ NEG_TRAILING = true
200
+ NEG_PAREN = true
201
+
202
+ class <<self
203
+
204
+ def into_money str
205
+ check_string str
206
+ ints, frac = decsplit str.dup
207
+ unless ints =~ /\d/ then
208
+ self::IDASH.each_char { |d|
209
+ if ints.end_with? d then
210
+ ints.slice! ints.length-1
211
+ break
212
+ end
213
+ }
214
+ end
215
+ unless frac =~ /\d/ then
216
+ self::DASH.each_char { |d|
217
+ if frac.start_with? d then
218
+ frac.slice! 0
219
+ break
220
+ end
221
+ }
222
+ end
223
+ digs = remove_grps [ints, frac]
224
+ if self::NEG_TRAILING then
225
+ digs.sub! /(\d.*)-/ do "-#$1" end
226
+ end
227
+ if self::NEG_PAREN then
228
+ digs.sub! /\((\d.*?)\)/ do "-#$1" end
229
+ end
230
+ Money.new digs
231
+ rescue ArgumentError, NoMethodError
232
+ raise ArgumentError, "Couldn't parse as a monetary: #{str}"
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ class Format
240
+
241
+ DASH = false # dash for zero in decimal part
242
+ IDASH = false # dash for zero in integer part
243
+ CHDASH = "-" # dash character (default '-')
244
+ NEG = :l # negative values: :p, :par = parentheses; :r, :right = right minus
245
+ CURSYM = "$" # currency symbol
246
+ SYMPOS = :l # symbol position: :l/:left (default), :r/:right, :n/:none
247
+ SYMSPC = false # space before/after currency symbol
248
+
249
+ class <<self
250
+
251
+ def from_money m
252
+ ints, frac = m.make_digits
253
+ i = new ints
254
+ i.do_curr frac, m.sign
255
+ i
256
+ end
257
+
258
+ end
259
+
260
+ def dash need
261
+ need or return
262
+ if @chars =~ /\A0+\z/ then
263
+ l = @chars.length
264
+ @chars.clear
265
+ @chars << cls::CHDASH
266
+ pad l, " "
267
+ end
268
+ end
269
+
270
+ def cursym rev
271
+ if rev then
272
+ case cls::SYMPOS
273
+ when :n, :none then return
274
+ when :r, :right then return
275
+ else nil
276
+ end
277
+ else
278
+ case cls::SYMPOS
279
+ when :n, :none then return
280
+ when :r, :right then nil
281
+ else return
282
+ end
283
+ end
284
+ cs = cls::CURSYM
285
+ cs = cs.reverse if rev
286
+ @chars << " " if cls::SYMSPC
287
+ @chars << cs
288
+ end
289
+
290
+ def curpresign n
291
+ if n.negative? then
292
+ case cls::NEG
293
+ when :p, :paren then @chars << "("
294
+ when :r, :right then nil
295
+ else @chars << "-"
296
+ end
297
+ end
298
+ end
299
+
300
+ def curpostsign n
301
+ if n.negative? then
302
+ case cls::NEG
303
+ when :p, :paren then @chars << ")"
304
+ when :r, :right then @chars << "-"
305
+ end
306
+ end
307
+ end
308
+
309
+ def do_curr frac, nsig
310
+ pad0 cls::NUMDIGITS||1
311
+ dash cls::IDASH
312
+ group
313
+ curpresign nsig
314
+ cursym true
315
+ pad_ cls::WIDTH
316
+ reverse!
317
+ do_decimal frac do |s,f|
318
+ f.dash cls::DASH
319
+ f.curpostsign nsig
320
+ f.cursym false
321
+ f.pad_ cls::RWIDTH
322
+ s.sep true
323
+ end
324
+ end
325
+
326
+ end
327
+
328
+
329
+ def Money v ; Money.new v ; end
330
+
331
+ end
332
+
333
+
334
+ class NilClass ; def to_money ; NumRepr::Money.new self ; end ; end
335
+ class Numeric ; def to_money ; NumRepr::Money.new self ; end ; end
336
+ class String ; def to_money ; NumRepr::Money.new self ; end ; end
337
+