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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f1a48451e0600ba32b7e705a6182e152b79c65eb2e2cd5b8f5972d78dd7dfd1f
4
+ data.tar.gz: afe9490a19dc479341ccf3fa367c5496eac1cedc6cec8236db39b6645cd2cfe6
5
+ SHA512:
6
+ metadata.gz: 68523527b522f8ea6beb3a29ff580b7cb27c19f7cf64ca3424a48b108f4ee1e1a5b8c1ec7360d702481f6dcadfbcfa3cb4699fd11cec6f704a428b4c530e0ba3
7
+ data.tar.gz: f59b0cfe695b372b506696816239bfd9b420b5d99ba849d7ec4dac340d8ef172e15b79a8b3b4fbf4dc80cb682c039b0d4d9fdc84cede39d7bc3205e143d6ab97
data/LICENSE ADDED
@@ -0,0 +1,52 @@
1
+ # BSD-2-clause license, extended by language use conditions
2
+
3
+ Copyright (C) 2026 Bertram Scharpf <software@bertram-scharpf.de>.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are
8
+ met:
9
+
10
+ * Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in
15
+ the documentation and/or other materials provided with the
16
+ distribution.
17
+
18
+ * Redistributions must not contain any clauses about anticipated
19
+ harassment or discrimination, nor must they be held in a so-called
20
+ "inclusive language". As far as German language is used, the
21
+ conditions mentioned below additionally apply.
22
+
23
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
24
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
25
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
26
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
27
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+
36
+ ## Use of the German Language
37
+
38
+ Beim Gebrauch deutscher Sprache sind Weiterentwicklungen und
39
+ -verbreitungen nur gestattet unter Einhaltung sowie abermaligen
40
+ Einforderns folgender zusätzlicher Bedingungen:
41
+
42
+ * Keine Verwendung von sogenannter „geschlechtergerechter Sprache“,
43
+ also Anfügen von weiblichen Endungen mit Binnen-I, Sternchen,
44
+ Doppelpunkt, Unterstrich oder ähnlichem, oder Konstruktionen, die
45
+ den Sachverhalt falsch wiedergeben („Radfahrende“, „Studierende“).
46
+
47
+ * Keine Verwendung der „reformierten Rechtschreibung“ von 1996,
48
+ insbesondere Doppel-S am Silbenende, „plazieren“ mit T, sowie
49
+ Großschreibung von Wendungen wie „des weiteren“.
50
+
51
+
52
+ <!-- vim:set ft=markdown : -->
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Ruby-NumRepr
2
+
3
+ Number representations: Conversion from and to strings for Integer, Float, and
4
+ BigDecimal classes.
5
+
6
+ Classes for monetary und percent values.
7
+
8
+
9
+
10
+ ## Installation
11
+
12
+ ```shell
13
+ sudo gem install numrepr
14
+ ```
15
+
16
+
17
+ ## Usage
18
+
19
+ Parsing and formatting integers:
20
+
21
+ ```ruby
22
+ require "numrepr/format"
23
+
24
+ i = NumRepr::Parser.into_i "12.345"
25
+ "#{NumRepr::Format.from_i i}" #=> "12345"
26
+ ```
27
+
28
+ Custom format:
29
+
30
+ ```ruby
31
+ require "numrepr/format"
32
+
33
+ class MyFmt < NumRepr::Format
34
+ NUMDIGITS = 0 # leave empty if zero
35
+ GRP = true
36
+ WIDTH = 8
37
+ end
38
+
39
+ [ 12345, -12345, 0].map { |i| "#{MyFmt.from_i i}" }
40
+ #=> [" 12,345", " -12,345", " "]
41
+ ```
42
+
43
+ The standard parser recognizes comma as decimal separator as well
44
+ as the dot. You may define a custom parser that demands either.
45
+
46
+ ```ruby
47
+ require "numrepr/format"
48
+
49
+ %w(12,345 12.345).map { |s| NumRepr::Parser.into_i s }
50
+ #=> [12345, 12345]
51
+
52
+ class ParseDot < NumRepr::Parser
53
+ GRP = "."
54
+ end
55
+ class ParseComma < NumRepr::Parser
56
+ GRP = ","
57
+ end
58
+
59
+ %w(12,345 12.345).map { |s| ParseDot.into_i s rescue nil }
60
+ #=> [nil, 12345]
61
+ %w(12,345 12.345).map { |s| ParseComma.into_i s rescue nil }
62
+ #=> [12345, nil]
63
+ ```
64
+
65
+ Parsing and formatting floats respectively decimals:
66
+
67
+ ```ruby
68
+ require "numrepr/format"
69
+ require "bigdecimal/util"
70
+
71
+ class FormatCustom < NumRepr::Format
72
+ # Change these and see what happens:
73
+ NUMDIGITS = 10
74
+ POS = false
75
+ GRP = true
76
+ WIDTH = 16
77
+ DEC = true # force decimal point
78
+ NUMPLACES = 0
79
+ RWIDTH = 8
80
+ ZSIGN = true
81
+ end
82
+
83
+ nums = %w(
84
+ 1.0 0.0 -0.0
85
+ 0.1 0.1234 0.00456 0.000456 0.0000456
86
+ 123456789.0 1230000.0
87
+ -1234.56789
88
+ NaN Infinity -Infinity
89
+ )
90
+
91
+ nums.each { |s|
92
+ puts "|#{FormatCustom::from_d s.to_d}|"
93
+ }
94
+ ```
95
+
96
+ Result:
97
+
98
+ ```
99
+ | 0,000,000,001. |
100
+ | 0,000,000,000. |
101
+ | -0,000,000,000. |
102
+ | 0,000,000,000.1 |
103
+ | 0,000,000,000.123,4 |
104
+ | 0,000,000,000.004,56 |
105
+ | 0,000,000,000.000,456 |
106
+ | 0,000,000,000.000,045,6|
107
+ | 0,123,456,789. |
108
+ | 0,001,230,000. |
109
+ | -0,000,001,234.567,89 |
110
+ | NaN |
111
+ | Inf |
112
+ | -Inf |
113
+ ```
114
+
115
+ Parsing:
116
+
117
+ ```ruby
118
+ require "numrepr/format"
119
+ require "bigdecimal/util"
120
+
121
+ %w(12,345.678,999 12.345,678.999).map { |s| NumRepr::Parser.into_d s }
122
+ #=> [12345.678999, 12345.678999]
123
+
124
+ class ParseDot < NumRepr::Parser
125
+ GRP = "."
126
+ end
127
+
128
+ %w(12,345.678,999 12.345,678.999).map { |s| ParseDot.into_d s rescue nil }
129
+ #=> [nil, 12345.678999]
130
+ ```
131
+
132
+
133
+ ### Monetary values
134
+
135
+ ```ruby
136
+ require "numrepr/money"
137
+ include NumRepr
138
+
139
+ class FormatEU < Format
140
+ CHDEC = ","
141
+ GRP = true
142
+ CHGRP = "."
143
+ DASH = true
144
+ CURSYM = "€"
145
+ end
146
+
147
+ amount = Parser.into_money "€ 12,345.-"
148
+ puts amount
149
+ puts "|#{FormatEU::from_money amount}|"
150
+ ```
151
+
152
+ Result:
153
+
154
+ ```
155
+ $12345.00
156
+ |€12.345,- |
157
+ ```
158
+
159
+ Default parsing:
160
+
161
+ ```ruby
162
+ require "numrepr/money"
163
+ include NumRepr
164
+
165
+ Parser.into_money "€ 12,345.-" #=> $12345.00
166
+ Parser.into_money "€ 12.345,-" #=> $12345.00
167
+ Parser.into_money "12.345,-€" #=> $12345.00
168
+ Parser.into_money "EUR 12.345,-" #=> $12345.00
169
+ Parser.into_money "12.345,-EUR" #=> $12345.00
170
+ Parser.into_money "12.345,67 EUR" #=> $12345.67
171
+ Parser.into_money "12.345,67€" #=> $12345.67
172
+ ```
173
+
174
+
175
+ ### Percent values
176
+
177
+ ```ruby
178
+ require "numrepr/money"
179
+ require "numrepr/percent"
180
+ include NumRepr
181
+
182
+ vat = "19%".to_percent
183
+ n = "100.00".to_money
184
+
185
+ n * vat #=> $19.00
186
+ vat.over n #=> $119.00
187
+ vat.split n #=> [$84.03, $15.97]
188
+ ```
189
+
190
+ If an amount cannot be split into a base and a percent increase,
191
+ it can be forced to the next lower number.
192
+
193
+ ```ruby
194
+ require "numrepr/money"
195
+ require "numrepr/percent"
196
+ include NumRepr
197
+
198
+ vat = "19%".to_percent
199
+ b = "69.99".to_money
200
+ vat.split b #=> raises NoSolutionError
201
+ n, t = vat.split! b #=> [$58.82, $11.17]
202
+ c = vat.over n #=> $70.00
203
+ vat.split c #=> [$58.82, $11.18]
204
+ ```
205
+
206
+
207
+ ## Copyright
208
+
209
+ * (C) 2026 Bertram Scharpf <software@bertram-scharpf.de>.
210
+ * License: [BSD-2-Clause+](./LICENSE)
211
+ * Repository: [ruby-numrepr](https://github.com/BertramScharpf/ruby-numrepr)
212
+
@@ -0,0 +1,296 @@
1
+ #
2
+ # decimal.rb -- Umlimited precision decimal
3
+ #
4
+
5
+ # This is just a study. Please use the standard +BigDecimal+.
6
+
7
+
8
+ require "numrepr/format"
9
+
10
+
11
+ module NumRepr
12
+
13
+ class Decimal
14
+
15
+ BASE = 10
16
+
17
+ @precision = 16
18
+
19
+ class <<self
20
+
21
+ attr_accessor :precision
22
+
23
+ alias parse new
24
+
25
+ def zero ; new nil ; end
26
+
27
+ def as obj
28
+ case obj
29
+ when self then obj
30
+ else new obj
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ attr_reader :mant, :expo
37
+
38
+ def initialize mant, expo = nil
39
+ @mant, @expo =
40
+ case mant
41
+ when Integer then [ mant, 0]
42
+ when Rational then parse_num mant.to_f
43
+ when Numeric then parse_num mant
44
+ else parse_str mant.to_s
45
+ end
46
+ @expo += expo.to_i if expo
47
+ norm!
48
+ rescue ArgumentError
49
+ raise ArgumentError, "Not a Decimal value: \"#{mant}\"."
50
+ end
51
+
52
+ def mag ; BASE ** @expo ; end
53
+ alias magnitude mag
54
+
55
+ private
56
+
57
+ def parse_num num
58
+ if num.respond_to? :split then # Will there ever be something like this for Float?
59
+ _, dig, _, exp = num.split
60
+ [ dig.to_i, exp - dig.length]
61
+ else
62
+ parse_str num.to_s
63
+ end
64
+ end
65
+
66
+ def parse_str str
67
+ m, e = str.split /e/i, 2
68
+ e = e ? (Integer e, BASE) : 0
69
+ m.sub! %r/\./ do
70
+ e -= $'.count "0-9"
71
+ nil
72
+ end
73
+ [ (Integer m, BASE), e]
74
+ end
75
+
76
+ public
77
+
78
+ def eql? oth ; self.class == oth.class and @mant == oth.mant and @expo == oth.expo ; end
79
+
80
+ def hash ; [@mant,@expo].hash ; end
81
+
82
+ def to_decimal ; self ; end
83
+
84
+ def to_f ; @mant.to_f * mag ; end
85
+ def to_i ; Integer @mant * mag ; end
86
+ def to_r ; Rational @mant * mag ; end
87
+ def to_d ; @mant.to_d * mag ; end
88
+
89
+ def succ! ; add! 1 ; end
90
+ def pred! ; sub! 1 ; end
91
+ def succ ; dup.succ! ; end
92
+ def pred ; dup.pred! ; end
93
+
94
+ def zero? ; @mant.zero? ; end
95
+ def nonzero? ; @mant.nonzero? and self ; end
96
+
97
+ def sign ; @mant ; end
98
+ def positive? ; @mant.positive? ; end
99
+ def negative? ; @mant.negative? ; end
100
+ alias pos? positive?
101
+ alias neg? negative?
102
+
103
+ private
104
+
105
+ def norm!
106
+ if @mant.zero? then
107
+ @expo = 0
108
+ else
109
+ loop do
110
+ m, r = @mant.divmod BASE
111
+ break if r.nonzero?
112
+ @mant = m
113
+ @expo += 1
114
+ end
115
+ end
116
+ self
117
+ end
118
+
119
+ def same_expo oth
120
+ oth = Decimal.new oth unless Decimal === oth
121
+ om, oe = oth.mant, oth.expo
122
+ while @expo > oe do
123
+ @mant *= BASE
124
+ @expo -= 1
125
+ end
126
+ while @expo < oe do
127
+ om *= BASE
128
+ oe -= 1
129
+ end
130
+ om
131
+ end
132
+
133
+ public
134
+
135
+ def add! oth ; m = same_expo oth ; @mant += m ; norm! ; end
136
+ def sub! oth ; m = same_expo oth ; @mant -= m ; norm! ; end
137
+ def add oth ; dup.add! oth ; end
138
+ def sub oth ; dup.sub! oth ; end
139
+
140
+ def mul! oth
141
+ oth = Decimal.new oth unless Decimal === oth
142
+ @mant *= oth.mant
143
+ @expo += oth.expo
144
+ norm!
145
+ end
146
+ def mul oth ; dup.mul! oth ; end
147
+
148
+ def div! oth, precision = 0
149
+ oth = Decimal.new oth unless Decimal === oth
150
+ @expo -= oth.expo
151
+ neg = @mant.negative?
152
+ @mant, n = 0, @mant.abs
153
+ t = (precision||self.class.precision).to_i
154
+ while t + @expo >= 0 do
155
+ n, r = n.divmod oth.mant
156
+ @mant += n
157
+ break if r.zero?
158
+ n = r
159
+ n *= BASE
160
+ @mant *= BASE
161
+ @expo -= 1
162
+ end
163
+ @mant = -@mant if neg
164
+ norm!
165
+ end
166
+ def div oth, precision = 0 ; dup.div! oth, precision ; end
167
+
168
+ def quo! oth, precision = nil ; div! oth, precision ; end
169
+ def quo oth, precision = nil ; dup.quo! oth, precision ; end
170
+
171
+ alias + add
172
+ alias - sub
173
+ alias * mul
174
+ alias / div
175
+
176
+ def neg! ; @mant = -@mant ; self ; end
177
+ def neg ; dup.neg! ; end
178
+ def +@ ; self ; end
179
+ def -@ ; neg ; end
180
+
181
+ def abs ; self.class.new @mant.abs, @expo ; end
182
+
183
+ def <=> oth
184
+ case oth
185
+ when Decimal then (@expo <=> oth.expo).nonzero? || (@mant <=> oth.mant)
186
+ when Numeric then (@mant.to_f * 10 ** @expo) <=> oth
187
+ end
188
+ end
189
+ include Comparable
190
+
191
+ def coerce oth ; [ oth, to_r] ; end
192
+
193
+ def round! precision = nil, method = nil
194
+ s = -@expo - precision.to_i
195
+ if s <= 0 then
196
+ self
197
+ else
198
+ neg = @mant.negative?
199
+ g = BASE ** s
200
+ @mant, r = @mant.abs.divmod g
201
+ @expo += s
202
+ r *= 2
203
+ up =
204
+ case method||:comm
205
+ when :comm then r >= g
206
+ when :stat then r > g || r == g && @mant.odd?
207
+ when :trunc then false
208
+ when :ceil then true
209
+ when :up then !neg
210
+ when :down then neg
211
+ else raise ArgumentError, "Unknown rounding method: #{method}"
212
+ end
213
+ @mant += 1 if up
214
+ @mant = -@mant if neg
215
+ norm!
216
+ end
217
+ end
218
+ def round precision = nil, method = nil ; dup.round! precision, method ; end
219
+
220
+ def trunc! precision = nil ; round! precision, :trunc ; end
221
+ def trunc precision = nil ; dup.trunc! precision ; end
222
+ def ceil! precision = nil ; round! precision, :ceil ; end
223
+ def ceil precision = nil ; dup.ceil! precision ; end
224
+
225
+ def make_digits
226
+ d, f = "", ""
227
+ n = @mant.abs
228
+ e = @expo < 0 ? @expo : 0
229
+ in_d = false
230
+ loop do
231
+ if e >= 0 then
232
+ in_d = true
233
+ end
234
+ if e < @expo then
235
+ r = 0
236
+ else
237
+ break if n.zero? and in_d
238
+ n, r = n.divmod 10
239
+ end
240
+ e += 1
241
+ (in_d ? d : f) << (r + "0".ord).chr
242
+ end
243
+ f.reverse!
244
+ [ d, f]
245
+ end
246
+
247
+ def raw
248
+ d, f = make_digits
249
+ d << "0" if d.empty?
250
+ d << "-" if negative?
251
+ d.reverse!
252
+ f.empty? or d << "." << f
253
+ d
254
+ end
255
+ alias to_s raw
256
+ def inspect
257
+ r = @mant.to_s
258
+ r << "e" << @expo.to_s unless @expo.zero?
259
+ r
260
+ end
261
+
262
+ end
263
+
264
+ class Parser
265
+
266
+ class <<self
267
+
268
+ def into_decimal str
269
+ digs = prepare_float str
270
+ Decimal digs
271
+ rescue ArgumentError, NoMethodError
272
+ raise ArgumentError, "Couldn't parse as a decimal: #{str}"
273
+ end
274
+
275
+ end
276
+
277
+ end
278
+
279
+ class Format
280
+
281
+ class <<self
282
+ alias from_decimal from_d
283
+ end
284
+
285
+ end
286
+
287
+
288
+ def Decimal v ; Decimal.new v ; end
289
+
290
+ end
291
+
292
+
293
+ class NilClass ; def to_decimal ; NumRepr::Decimal.new self ; end ; end
294
+ class Numeric ; def to_decimal ; NumRepr::Decimal.new self ; end ; end
295
+ class String ; def to_decimal ; NumRepr::Decimal.new self ; end ; end
296
+