riel 1.0.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.
Files changed (48) hide show
  1. data/README +0 -0
  2. data/lib/riel/ansicolor.rb +93 -0
  3. data/lib/riel/array.rb +20 -0
  4. data/lib/riel/command.rb +30 -0
  5. data/lib/riel/date.rb +16 -0
  6. data/lib/riel/dir.rb +90 -0
  7. data/lib/riel/enumerable.rb +66 -0
  8. data/lib/riel/env.rb +49 -0
  9. data/lib/riel/file.rb +212 -0
  10. data/lib/riel/filetype.rb +189 -0
  11. data/lib/riel/hash.rb +12 -0
  12. data/lib/riel/io.rb +20 -0
  13. data/lib/riel/log.rb +548 -0
  14. data/lib/riel/matchdata.rb +13 -0
  15. data/lib/riel/optproc.rb +369 -0
  16. data/lib/riel/pathname.rb +16 -0
  17. data/lib/riel/rcfile.rb +35 -0
  18. data/lib/riel/regexp.rb +152 -0
  19. data/lib/riel/setdiff.rb +53 -0
  20. data/lib/riel/size_converter.rb +62 -0
  21. data/lib/riel/string.rb +81 -0
  22. data/lib/riel/tempfile.rb +28 -0
  23. data/lib/riel/text.rb +408 -0
  24. data/lib/riel/timer.rb +52 -0
  25. data/lib/riel.rb +13 -0
  26. data/test/riel/array_test.rb +22 -0
  27. data/test/riel/command_test.rb +28 -0
  28. data/test/riel/date_test.rb +17 -0
  29. data/test/riel/dir_test.rb +98 -0
  30. data/test/riel/enumerable_test.rb +27 -0
  31. data/test/riel/env_test.rb +52 -0
  32. data/test/riel/file_test.rb +242 -0
  33. data/test/riel/filetype_test.rb +32 -0
  34. data/test/riel/hash_test.rb +12 -0
  35. data/test/riel/io_test.rb +22 -0
  36. data/test/riel/log_test.rb +184 -0
  37. data/test/riel/matchdata_test.rb +15 -0
  38. data/test/riel/optproc_test.rb +233 -0
  39. data/test/riel/pathname_test.rb +36 -0
  40. data/test/riel/rcfile_test.rb +44 -0
  41. data/test/riel/regexp_test.rb +24 -0
  42. data/test/riel/setdiff_test.rb +26 -0
  43. data/test/riel/size_converter_test.rb +64 -0
  44. data/test/riel/string_test.rb +58 -0
  45. data/test/riel/tempfile_test.rb +16 -0
  46. data/test/riel/text_test.rb +102 -0
  47. data/test/riel/timer_test.rb +43 -0
  48. metadata +134 -0
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+ require 'riel/env'
5
+ require 'riel/log'
6
+ require 'riel/text'
7
+ require 'riel/enumerable'
8
+
9
+
10
+ module OptProc
11
+
12
+ class Option
13
+ include Loggable
14
+
15
+ attr_reader :md, :tags, :res
16
+
17
+ ARG_INTEGER = %r{^ ([\-\+]?\d+) $ }x
18
+ ARG_FLOAT = %r{^ ([\-\+]?\d* (?:\.\d+)?) $ }x
19
+ ARG_STRING = %r{^ [\"\']? (.*?) [\"\']? $ }x
20
+ ARG_BOOLEAN = %r{^ (yes|true|on|no|false|off) $ }ix
21
+
22
+ ARG_TYPES = Array.new
23
+ ARG_TYPES << [ :integer, ARG_INTEGER ]
24
+ ARG_TYPES << [ :float, ARG_FLOAT ]
25
+ ARG_TYPES << [ :string, ARG_STRING ]
26
+ ARG_TYPES << [ :boolean, ARG_BOOLEAN ]
27
+
28
+ def initialize(args = Hash.new, &blk)
29
+ @tags = args[:tags] || Array.new
30
+ @rc = args[:rc]
31
+ @rc = [ @rc ] if @rc.kind_of?(String)
32
+ @md = nil
33
+ @set = blk || args[:set]
34
+
35
+ @type = nil
36
+ @valuere = nil
37
+
38
+ @argtype = nil
39
+
40
+ @res = args[:res]
41
+ @res = [ @res ] if @res.kind_of?(Regexp)
42
+
43
+ if args[:arg]
44
+ # log { "args.class: #{args[:arg].class}" }
45
+ demargs = args[:arg].dup
46
+ while arg = demargs.shift
47
+ # log { "arg: #{arg}" }
48
+ case arg
49
+ when :required
50
+ @type = "required"
51
+ when :optional
52
+ @type = "optional"
53
+ when :none
54
+ @type = nil
55
+ when :regexp
56
+ @valuere = demargs.shift
57
+ else
58
+ if re = ARG_TYPES.assoc(arg)
59
+ # log { "re: #{re}" }
60
+ @valuere = re[1]
61
+ @argtype = arg
62
+ @type ||= "required"
63
+ else
64
+ # log { "no expression for arg #{arg}" }
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # log { "valuere: #{@valuere}" }
71
+ # log { "type: #{@type}" }
72
+ end
73
+
74
+ def inspect
75
+ '[' + @tags.collect { |t| t.inspect }.join(" ") + ']'
76
+ end
77
+
78
+ def to_str
79
+ to_s
80
+ end
81
+
82
+ def to_s
83
+ @tags.join(" ")
84
+ end
85
+
86
+ def match_rc?(field)
87
+ @rc && @rc.include?(field)
88
+ end
89
+
90
+ def match_value(val)
91
+ # log { "valuere: #{@valuere.inspect}; val: #{val}" }
92
+ @md = @valuere && @valuere.match(val)
93
+ # log { "md: #{@md.inspect}" }
94
+ @md && @md[1]
95
+ end
96
+
97
+ def match_tag(tag)
98
+ stack { "@rc: #{@rc.inspect}; @tags: #{@tags.inspect}" }
99
+
100
+ if tm = @tags.detect do |t|
101
+ log { "t: #{t}; tag: #{tag}; idx: #{t.index(tag)}" }
102
+ t.index(tag) == 0 && tag.length <= t.length
103
+ end
104
+
105
+ log { "tm: #{tm}" }
106
+ if tag.length == tm.length
107
+ 1.0
108
+ else
109
+ len = tag.length.to_f * 0.01 # / tm.length
110
+ log { "len: #{len}" }
111
+ len
112
+ end
113
+ else
114
+ nil
115
+ end
116
+ end
117
+
118
+ def match(args, opt = args[0])
119
+ return nil unless %r{^-}.match(opt)
120
+
121
+ # log { "opt: #{opt.inspect}; args: #{args.inspect}" }
122
+ # log { "@rc: #{@rc.inspect}; @re: #{@re.inspect}; @tags: #{@tags.inspect}" }
123
+
124
+ tag, val = opt.split('=', 2)
125
+ tag ||= opt
126
+
127
+ # log { "opt: #{opt}; opt: #{opt.class}; tag: #{tag}; tags: #{@tags.inspect}" }
128
+ # log { "res: #{@res.inspect}" }
129
+
130
+ @md = nil
131
+
132
+ if @res && (@md = @res.collect { |re| re.match(opt) }.detect)
133
+ # log { "matched: #{@md}" }
134
+ 1.0
135
+ else
136
+ match_tag(tag)
137
+ end
138
+ end
139
+
140
+ def set_value(args, opt = args[0])
141
+ tag, val = opt.split('=', 2)
142
+ args.shift
143
+
144
+ # log { "opt : #{opt}" }
145
+ # log { "tag : #{tag}" }
146
+ # log { "tags: #{@tags.inspect}" }
147
+ # log { "val : #{val.inspect}" }
148
+ # log { "md : #{@md.inspect}" }
149
+
150
+ if @md
151
+ # log { "already have match data" }
152
+ elsif @type == "required"
153
+ if val
154
+ # already have value
155
+ # log { "already have value: #{val}" }
156
+ elsif args.size > 0
157
+ val = args.shift
158
+ # log { "got next value: #{val}" }
159
+ else
160
+ $stderr.puts "value expected"
161
+ end
162
+
163
+ if val
164
+ match_value(val)
165
+ end
166
+ elsif @type == "optional"
167
+ if val
168
+ # log { "already have value: #{val}" }
169
+ match_value(val)
170
+ elsif args.size > 0
171
+ if %r{^-}.match(args[0])
172
+ # log { "skipping next value; apparently option" }
173
+ elsif match_value(args[0])
174
+ # log { "value matches: #{val}" }
175
+ args.shift
176
+ else
177
+ # log { "value does not match" }
178
+ end
179
+ end
180
+ else
181
+ # log { "no type" }
182
+ end
183
+
184
+ value = value_from_match
185
+
186
+ set(value, opt, args)
187
+ end
188
+
189
+ def value_from_match
190
+ if @md
191
+ if @argtype.nil? || @argtype == :regexp
192
+ @md
193
+ else
194
+ convert_value(@md[1])
195
+ end
196
+ elsif @argtype == :boolean
197
+ true
198
+ end
199
+ end
200
+
201
+ def convert_value(val)
202
+ if val
203
+ case @argtype
204
+ when :string
205
+ val
206
+ when :integer
207
+ val.to_i
208
+ when :float
209
+ val.to_f
210
+ when :boolean
211
+ to_boolean(val)
212
+ when :regexp
213
+ val
214
+ when nil
215
+ val
216
+ else
217
+ log { "unknown argument type: #{@type.inspect}" }
218
+ end
219
+ elsif @argtype == :boolean
220
+ true
221
+ end
222
+ end
223
+
224
+ def to_boolean(val)
225
+ %w{ yes true on soitenly }.include?(val.downcase)
226
+ end
227
+
228
+ def set(val, opt = nil, args = nil)
229
+ # log { "argtype: #{@argtype}; md: #{@md.inspect}" }
230
+
231
+ setargs = [ val, opt, args ].select_with_index { |x, i| i < @set.arity }
232
+ # log "val: #{val}"
233
+ @set.call(*setargs)
234
+ end
235
+ end
236
+
237
+
238
+ class OptionSet
239
+ include Loggable
240
+
241
+ attr_reader :options
242
+
243
+ def initialize(data)
244
+ @options = Array.new
245
+ @shortopts = Array.new
246
+ @longopts = Array.new
247
+ @regexps = Hash.new
248
+
249
+ data.each do |optdata|
250
+ opt = OptProc::Option.new(optdata)
251
+ @options << opt
252
+
253
+ opt.tags.each do |tag|
254
+ ch = tag[0]
255
+ if ch == 45 # 45 = '-'
256
+ ch = tag[1]
257
+ assocopts = nil
258
+ if ch == tag
259
+ ch = tag[2]
260
+ assocopts = @longopts
261
+ else
262
+ assocopts = @shortopts
263
+ end
264
+
265
+ (assocopts[ch] ||= Array.new) << opt
266
+ end
267
+
268
+ if res = opt.res
269
+ res.each do |re|
270
+ (@regexps[re] ||= Array.new) << opt
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ if false
277
+ [ @longopts, @shortopts ].each do |list|
278
+ list.each_with_index do |v, idx|
279
+ log { "#{idx} => #{v.inspect}" }
280
+ end
281
+ end
282
+ [ @regexps ].each do |map|
283
+ map.each do |k, v|
284
+ log { "#{k} => #{v.inspect}" }
285
+ end
286
+ end
287
+ end
288
+
289
+ end
290
+
291
+ COMBINED_OPTS_RES = [
292
+ # -number non-num, then anything
293
+ Regexp.new('^ ( - \d+ ) ( \D+.* ) $ ', Regexp::EXTENDED),
294
+ # -letter anything
295
+ Regexp.new('^ ( - [a-z] ) ( .+ ) $ ', Regexp::EXTENDED)
296
+ ]
297
+
298
+ def process_option(args)
299
+ opt = args[0]
300
+
301
+ # log { "processing option #{opt}" }
302
+
303
+ if md = COMBINED_OPTS_RES.collect { |re| re.match(opt) }.detect
304
+ lhs = md[1]
305
+ rhs = "-" + md[2]
306
+
307
+ # log { "lhs, rhs: #{lhs.inspect}, #{rhs.inspect}" }
308
+
309
+ args[0, 1] = lhs, rhs
310
+
311
+ return process_option(args)
312
+ elsif opt[0] == 45
313
+ ch = opt[1]
314
+ assocopts = if ch == 45 # 45 = '-'
315
+ ch = opt[2]
316
+ @longopts[ch]
317
+ elsif ch.nil?
318
+ nil
319
+ else
320
+ @shortopts[ch]
321
+ end
322
+
323
+ # log { "opts: #{assocopts.inspect}" }
324
+ if assocopts && x = set_option(assocopts, args)
325
+ return x
326
+ end
327
+ end
328
+
329
+ if x = set_option(@options, args)
330
+ return x
331
+ elsif @bestmatch
332
+ # what's the best match here ...?
333
+ log { "bestmatch: #{@bestmatch}" }
334
+ log { "bestopts : #{@bestopts.inspect}" }
335
+ if @bestopts.size == 1
336
+ @bestopts[0].set_value(args)
337
+ return @bestopts[0]
338
+ else
339
+ optstr = @bestopts.collect { |x| '(' + x.tags.join(', ') + ')' }.join(', ')
340
+ $stderr.puts "ERROR: ambiguous match of '#{args[0]}'; matches options: #{optstr}"
341
+ exit 2
342
+ end
343
+ end
344
+
345
+ nil
346
+ end
347
+
348
+ def set_option(optlist, args)
349
+ @bestmatch = nil
350
+ @bestopts = Array.new
351
+
352
+ optlist.each do |option|
353
+ if mv = option.match(args)
354
+ if mv >= 1.0
355
+ # exact match:
356
+ option.set_value(args)
357
+ return option
358
+ elsif !@bestmatch || @bestmatch <= mv
359
+ @bestmatch = mv
360
+ @bestopts << option
361
+ end
362
+ end
363
+ end
364
+ nil
365
+ end
366
+
367
+ end
368
+
369
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+ require 'pathname'
5
+ require 'riel/string'
6
+
7
+
8
+ class Pathname
9
+
10
+ # a compliment to the +dirname+, +basename+, and +extname+ family, this returns
11
+ # the basename without the extension, e.g. "foo" from "/usr/share/lib/foo.bar".
12
+ def rootname
13
+ basename.to_s - extname.to_s
14
+ end
15
+
16
+ end
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+
5
+ # Represents a resource file, where '#' is used to comment to end of lines, and
6
+ # name/value pairs are separated by '=' or ':'.
7
+
8
+ class RCFile
9
+
10
+ attr_reader :settings
11
+
12
+ # Reads the RC file, if it exists, and if a block is passed, calls the block
13
+ # with each name/value pair, which are also accessible via
14
+ # <code>settings</code>.
15
+
16
+ def initialize(fname, &blk)
17
+ @settings = Array.new
18
+
19
+ if File.exists?(fname)
20
+ IO.readlines(fname).each do |line|
21
+ line.sub!(/\s*#.*/, "")
22
+ line.chomp!
23
+ name, value = line.split(/\s*[=:]\s*/)
24
+ if name && value
25
+ name.strip!
26
+ value.strip!
27
+ @settings << [ name, value ]
28
+ if blk
29
+ blk.call(name, value)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+ # Negates the given expression.
5
+ class NegatedRegexp < Regexp
6
+
7
+ def match(str)
8
+ !super
9
+ end
10
+
11
+ end
12
+
13
+ class Regexp
14
+
15
+ # shell expressions to Ruby regular expression sequences
16
+ SH2RE = Hash[
17
+ '*' => '.*',
18
+ '?' => '.',
19
+ # '[' => '\[',
20
+ # ']' => '\]',
21
+ '.' => '\.',
22
+ '$' => '\$',
23
+ '/' => '\/',
24
+ '(' => '\(',
25
+ ')' => '\)',
26
+ ]
27
+
28
+ # Returns a regular expression for the given Unix file system expression.
29
+
30
+ def self.unixre_to_string(pat)
31
+ pat.gsub(%r{(\\.)|(.)}) do
32
+ $1 || SH2RE[$2] || $2
33
+ end
34
+ end
35
+
36
+ WORD_START_RE = Regexp.new('^ # start of word
37
+ [\[\(]* # parentheses or captures, maybe
38
+ (?: \\\w | \\w) # literal \w, or what \w matches
39
+ ',
40
+ Regexp::EXTENDED)
41
+
42
+ WORD_END_RE = Regexp.new('(?: # one of the following:
43
+ \\\w # - \w for regexp
44
+ | #
45
+ \w # - a literal A-Z, a-z, 0-9, or _
46
+ | #
47
+ (?: # - one of the following:
48
+ \[[^\]]* # LB, with no RB until:
49
+ (?: # - either of:
50
+ \\w # - "\w"
51
+ | #
52
+ \w # - a literal A-Z, a-z, 0-9, or _
53
+ ) #
54
+ [^\]]*\] # - anything (except RB) to the next RB
55
+ ) #
56
+ ) #
57
+ (?: # optionally, one of the following:
58
+ \* # - "*"
59
+ | #
60
+ \+ # - "+"
61
+ | #
62
+ \? # - "?"
63
+ | #
64
+ \{\d*,\d*\} # - "{3,4}", "{,4}, "{,123}" (also matches the invalid {,})
65
+ )? #
66
+ $ # fin
67
+ ',
68
+ Regexp::EXTENDED)
69
+
70
+ # Handles negation, whole words, and ignore case (Ruby no longer supports
71
+ # Rexexp.new(/foo/i), as of 1.8).
72
+
73
+ def self.create(pat, args = Hash.new)
74
+ negated = args[:negated]
75
+ ignorecase = args[:ignorecase]
76
+ wholewords = args[:wholewords]
77
+ wholelines = args[:wholelines]
78
+ extended = args[:extended]
79
+ multiline = args[:multiline]
80
+
81
+ pattern = pat.dup
82
+
83
+ # we handle a ridiculous number of possibilities here:
84
+ # /foobar/ -- "foobar"
85
+ # /foo/bar/ -- "foo", then slash, then "bar"
86
+ # /foo\/bar/ -- same as above
87
+ # /foo/bar/i -- same as above, case insensitive
88
+ # /foo/bari -- "/foo/bari" exactly
89
+ # /foo/bar\/i -- "/foo/bar/i" exactly
90
+ # foo/bar/ -- "foo/bar/" exactly
91
+ # foo/bar/ -- "foo/bar/" exactly
92
+
93
+ if pattern.sub!(%r{ ^ !(?=/) }x, "")
94
+ negated = true
95
+ end
96
+
97
+ if pattern.sub!(%r{ ^ \/ (.*[^\\]) \/ ([mix]+) $ }x) { $1 }
98
+ modifiers = $2
99
+
100
+ multiline ||= modifiers.index('m')
101
+ ignorecase ||= modifiers.index('i')
102
+ extended ||= modifiers.index('x')
103
+ else
104
+ pattern.sub!(%r{ ^\/ (.*[^\\]) \/ $ }x) { $1 }
105
+ end
106
+
107
+ if wholewords
108
+ # sanity check:
109
+
110
+ errs = [
111
+ [ WORD_START_RE, "start" ],
112
+ [ WORD_END_RE, "end" ]
113
+ ].collect do |ary|
114
+ re, err = *ary
115
+ re.match(pattern) ? nil : err
116
+ end.compact
117
+
118
+ if errs.length > 0
119
+ Log.warn "pattern '#{pattern}' does not " + errs.join(" and ") + " on a word boundary"
120
+ end
121
+ pattern = '\b' + pattern + '\b'
122
+ elsif wholelines
123
+ pattern = '^' + pattern + '$' # ' for emacs
124
+ end
125
+
126
+ reclass = negated ? NegatedRegexp : Regexp
127
+
128
+ flags = [
129
+ [ ignorecase, Regexp::IGNORECASE ],
130
+ [ extended, Regexp::EXTENDED ],
131
+ [ multiline, Regexp::MULTILINE ]
132
+ ].inject(0) do |tot, ary|
133
+ val, flag = *ary
134
+ tot | (val ? flag : 0)
135
+ end
136
+
137
+ reclass.new(pattern, flags)
138
+ end
139
+
140
+ def self.matches_word_start?(pat)
141
+ WORD_START_RE.match(pat)
142
+ end
143
+
144
+ def self.matches_word_end?(pat)
145
+ WORD_END_RE.match(pat)
146
+ end
147
+
148
+ # applies Perl-style substitution (s/foo/bar/).
149
+ def self.perl_subst(pat)
150
+ end
151
+
152
+ end
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+ # Compares two enumerables, treating them as sets, showing whether they are
5
+ # identical, A contains B, B contains A, or A and B contain common elements.
6
+
7
+ class SetDiff
8
+ def SetDiff.new(a, b)
9
+ allitems = a | b
10
+
11
+ a_and_b = Array.new
12
+ a_not_in_b = Array.new
13
+ b_not_in_a = Array.new
14
+
15
+ allitems.each do |it|
16
+ if a.include?(it)
17
+ if b.include?(it)
18
+ a_and_b
19
+ else
20
+ a_not_in_b
21
+ end
22
+ else
23
+ b_not_in_a
24
+ end << it
25
+ end
26
+
27
+ super(a_and_b, a_not_in_b, b_not_in_a)
28
+ end
29
+
30
+ attr_reader :a_and_b, :a_not_in_b, :b_not_in_a
31
+
32
+ def initialize(a_and_b, a_not_in_b, b_not_in_a)
33
+ @a_and_b = a_and_b
34
+ @a_not_in_b = a_not_in_b
35
+ @b_not_in_a = b_not_in_a
36
+ end
37
+
38
+ def diff_type
39
+ @diff_type ||= if @a_and_b.empty?
40
+ :no_common
41
+ elsif @a_not_in_b.empty?
42
+ if @b_not_in_a.empty?
43
+ :identical
44
+ else
45
+ :b_contains_a
46
+ end
47
+ elsif @b_not_in_a.empty?
48
+ :a_contains_b
49
+ else
50
+ :common
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/ruby -w
2
+ # -*- ruby -*-
3
+
4
+ class SizeConverter
5
+ # http://www.gnu.org/software/coreutils/manual/html_node/Block-size.html
6
+
7
+ # don't round to closest -- just convert
8
+ def self.convert_to_kilobytes(size, decimal_places = 1)
9
+ ### SizeConverter._convert(Human::CONVERSIONS, 2, size, decimal_places)
10
+ end
11
+
12
+ class Human
13
+ CONVERSIONS = [
14
+ [ 12, "T" ],
15
+ [ 9, "G" ],
16
+ [ 6, "M" ],
17
+ [ 3, "K" ]
18
+ ]
19
+
20
+ # returns a string representation of the size. Note that K, G, M are
21
+ # gibibytes, etc., that is, powers of 10.
22
+
23
+ def self.convert(size, decimal_places = 1)
24
+ SizeConverter._convert(CONVERSIONS, 10, size, decimal_places)
25
+ end
26
+ end
27
+
28
+ class SI
29
+ # http://physics.nist.gov/cuu/Units/binary.html
30
+ CONVERSIONS = [
31
+ [ 40, "TiB" ],
32
+ [ 30, "GiB" ],
33
+ [ 20, "MiB" ],
34
+ [ 10, "KiB" ]
35
+ ]
36
+
37
+ # returns a string representation of the size. Note that K, G, M are
38
+ # gigabytes, etc.
39
+
40
+ def self.convert(size, decimal_places = 1)
41
+ SizeConverter._convert(CONVERSIONS, 2, size, decimal_places)
42
+ end
43
+ end
44
+
45
+ # legacy:
46
+
47
+ def self.convert(size, decimal_places = 1)
48
+ Human::convert(size, decimal_places)
49
+ end
50
+
51
+ def self._convert(conversions, base, size, decimal_places)
52
+ sizef = size.to_f
53
+ conversions.each do |conv|
54
+ sz = sizef / (base ** conv[0])
55
+ if sz >= 1.0
56
+ return sprintf("%.*f%s", decimal_places, sz, conv[1])
57
+ end
58
+ end
59
+
60
+ sprintf("%.*f", decimal_places, size)
61
+ end
62
+ end