Updated RubySpec submodule to 9f66d0b1.
[rbx.git] / kernel / core / sprintf.rb
blobc969682a3d55ec5cd931908dc7477623fd81b595
1 # depends on: module.rb class.rb
3 class Sprintf
4   
5   attr_accessor :fmt
6   attr_accessor :args
7   attr_accessor :flags
8   
9   RADIXES = {"b" => 2, "o" => 8, "d" => 10, "x" => 16}
10   ALTERNATIVES = {"o" => "0", "b" => "0b", "B" => "0B", "x" => "0x", "X" => "0X"}
11   PrecisionMax = 1048576 # Totally random value
12   
13   def initialize(fmt, *args)
14     @tainted = fmt.tainted?
15     @fmt, @args, @arg_position = fmt.to_str, args, 0
16   end
17   
18   def parse
19     start = 0
20     ret = ""
21     width = nil
22     precision = nil
23     @positional = false
24     @relative = false
25     @arg_position = 0
27     while (match = /%/.match_from(fmt, start))
29       @flags = {:space => nil, :position => nil, :alternative => nil, :plus => nil, 
30         :minus => nil, :zero => nil, :star => nil}
31       @width = @precision = @type = nil
33       ret << match.pre_match_from(start)
34       start = match.begin(0) + 1
36       # Special case: %% prints out as "%"
37       if [?\n, 0].include?(@fmt[start])
38         ret << "%" << @fmt[start]
39         start += 1
40         next
41       elsif [?%, nil].include?(@fmt[start])
42         ret << "%"
43         start += 1
44         next
45       elsif @fmt[start, 3] =~ /[1-9]\$/ && !@fmt[start + 2]
46         ret << "%"
47         start = @fmt.size
48         break
49       end
50       
51       # FLAG STATE
52       while token = /\G( |[1-9]\$|#|\+|\-|0|\*)/.match_from(fmt, start)
53         case token[0]
54         # Special case: if we get two [1-9]\$, it means that we're outside of flag-land
55         when /[1-9]\$/
56           raise ArgumentError, "value given twice - #{token[0]}" if flags[:position]
57           @flags[:position] = token[0][0].chr.to_i
58           start += 1
59         when " "
60           @flags[:space] = true
61         when "#"
62           @flags[:alternative] = true
63         when "+"
64           @flags[:plus] = true
65         when "-"
66           @flags[:minus] = true
67         when "0"
68           @flags[:zero] = true
69         when "*"
70           raise ArgumentError, "width given twice" if flags[:star]
71           if width_dollar_match = /\G[1-9]\$/.match_from(fmt, start + 1)
72             @width = Slot.new('*' << width_dollar_match[0])
73             start += 2
74           end
75           @flags[:star] = true
76         end
77         start += 1
78       end
79       
80       # WIDTH STATE
81       if !flags[:star] && width_match = /\G([1-9]\$|\*|\d+)/.match_from(fmt, start)
82         @width = Slot.new(width_match[0])
83         start += width_match[0].size
84       end
85     
86       # PRECISION DETERMINATION STATE
87       if /\G\./.match_from(fmt, start)
88         start += 1
89         # PRECISION STATE
90         if /\G\*/.match_from(fmt, start)
91           if precision_dollar_match = /\G[1-9]\$/.match_from(fmt, start + 1)
92             @precision = Slot.new('*' << precision_dollar_match[0])
93             start += 3
94           else
95             @precision = Slot.new('*')
96             start += 1
97           end
98         elsif precision_match = /\G([1-9]\$|\d+)/.match_from(fmt, start)
99           @precision = Slot.new(precision_match[0])
100           start += precision_match[0].size
101         else
102           @precision = Slot.new("0")
103         end
105         # check for positional value again, after the optional '.'
106         if positional_match = /\G[1-9]\$/.match_from(fmt, start)
107           raise ArgumentError, "value given twice - #{token[0]}" if flags[:position]
108           @flags[:position] = positional_match[0][0].chr.to_i
109           start += 2
110         end
111       end
112     
113       # TYPE STATE
114       unless type = /\G[bcdEefGgiopsuXx]/i.match_from(fmt, start)
115         raise ArgumentError, "malformed format string - missing field type" 
116       else
117         @type = type[0]
118         start += 1
119       end
120       
121       # Next: Use the parsed values to format some stuff :)
122       f = format
123       ret << f if f
124     end
125     if $DEBUG == true && !@positional
126       raise ArgumentError, "you need to use all the arguments" unless @arg_position == args.size
127     end
128     ret << @fmt[start..-1] if start < @fmt.size
129     ret.taint if @tainted
130     ret
131   end
132   
133   def format
134     # GET VALUE
135     if flags[:position]
136       val = Slot.new("#{flags[:position]}$")
137       val = get_arg(val)            
138     end
139     
140     # GET WIDTH
141     @width = Slot.new("*") if flags[:star] && !@width
142     width = get_arg(@width)
143     width = width.to_int if width.respond_to?(:to_int)
144     if width && width < 0
145       width = width.abs
146       flags[:minus] = true
147     end
148     
149     # GET PRECISION
150     precision = get_arg(@precision)
151     precision = precision.to_int if precision.respond_to?(:to_int)
152     
153     unless flags[:position]
154       val = Slot.new("*")
155       val = get_arg(val)
156     end
157     
158     case @type
159     when "e", "E", "f", "g", "G"
160       if @type.downcase == "g" && flags[:alternative]
161         @old_type = "g"
162         @type = "f"
163         precision = 4 unless precision
164       end
165       val = Float(val)
166       if val.finite?
167         ret = val.send(:to_s_formatted, build_format_string(width, precision))
168         ret = plus_char + ret if val >= 0 && @old_type
169       else
170         ret = (val < 0 ? "-Inf" : "Inf") if val.infinite?
171         ret = "NaN" if val.nan?
172         ret = plus_char + ret if val > 0
173         flags[:zero] = flags[:space] = flags[:plus] = nil
174         ret = pad(ret, width, precision)
175       end
176     when "u"
177       val = get_number(val)
178       if val < 0
179         unless val.kind_of?(Fixnum)
180           raise ArgumentError, "invalid type (only Fixnum allowed)"
181         end
182         
183         val = (2**(2.size * 8)) + val
184         if !flags[:zero] and !precision
185           ret = "..#{pad(val, width, precision)}"
186         else
187           ret = pad(val, width, precision)
188         end
189       else
190         ret = pad(val, width, precision)
191       end
192     when "d", "i"
193       val = get_number(val)
194       ret = pad(val, width, precision)
195     when "c"
196       val = val.to_int if val.respond_to?(:to_int)
197       raise TypeError, "cannot convert #{val.class} into Integer" unless val.respond_to?(:chr) && val.respond_to?(:%)
198       val = (val % 256).chr
199       ret = pad(val, width, precision)
200     when "s"
201       flags[:zero] = flags[:space] = flags[:plus] = nil
202       ret = pad(val, width, precision)
203       ret.taint if val.tainted?
204     when "p"
205       flags[:zero] = flags[:space] = flags[:plus] = nil
206       ret = pad(val.inspect, width, precision)      
207     when "o", "x", "X", "b", "B"
208       val = get_number(val)
209       unless flags[:space] || flags[:plus]
210         ret = Number.new(val, RADIXES[@type.downcase]).rep
211         chr = val < 0 ? (RADIXES[@type.downcase] - 1).to_s(RADIXES[@type.downcase]) : 0.to_s
212         ret = pad(ret, width, precision, chr)
213         ret = ALTERNATIVES[@type].to_s + ret if flags[:alternative]
214       else
215         flags[:plus] = nil if val < 0
216         ret = val.to_s(RADIXES[@type.downcase])
217         ret.gsub!(/^(\-?)/, "\1#{ALTERNATIVES[@type]}") if flags[:alternative]
218         ret = pad(ret, width, precision)
219         ret.gsub!(/ \-/, "-")
220       end
221       ret = ret.downcase if @type == "x"
222       ret = ret.upcase if @type == "X"
223     end
224     ret
225   end
226   
227   def get_number(val)
228     unless val.respond_to?(:full_to_i)
229       if val.respond_to?(:to_int)        
230         val = val.to_int
231       elsif val.respond_to?(:to_i)
232         val = val.to_i
233       end
234     end
235     val = val.full_to_i if val.respond_to?(:full_to_i)    
236     val = 0 if val.nil?
237     val
238   end
239   
240   def build_format_string(width, precision)
241     ret = "%#{make_flags}#{width}"
242     ret << ".#{precision}" if precision
243     ret << @type
244     ret
245   end
246   
247   def make_flags
248     ret = ""
249     ret << " " if flags[:space]
250     ret << "#" if flags[:alternative]
251     ret << "+" if flags[:plus]
252     ret << "-" if flags[:minus]
253     ret << "0" if flags[:zero]
254     ret
255   end
256   
257   def get_arg(slot)
258     return nil unless slot
259     if slot.position == :next
260       raise ArgumentError, "unnumbered mixed with numbered" if @positional
261       @relative = true
262       raise ArgumentError, "you ran out of arguments" if @arg_position >= args.size
263       ret = args[@arg_position]
264       @arg_position += 1
265     elsif slot.pos
266       raise ArgumentError, "unnumbered mixed with numbered" if @relative      
267       @positional = true
268       ret = args[slot.position - 1]
269     elsif slot.value
270       raise ArgumentError, "unnumbered mixed with numbered" if @positional      
271       @relative = true
272       ret = slot.value
273     end
274     raise ArgumentError, "you specified an argument position that did not exist: #{slot.str}" unless defined?(ret)
275     ret
276   end
277   
278   def pad(val, width, precision, pad_override = nil)
279     direction = flags[:minus] ? :ljust : :rjust
280     ret = val.to_s
281     modded_width = width.to_i + (flags[:plus] ? 1 : 0)
282     width = nil if modded_width <= val.to_s.size
283     if precision || flags[:zero]
284       ret.gsub!("..", "")
285     end
286     if precision
287       if precision > PrecisionMax
288         raise ArgumentError, "precision too big"
289       end
290       ret = plus_char + ret.send(direction, precision, pad_override || "0") 
291       flags[:zero] = flags[:plus] = flags[:space] = nil
292     end
293     if width
294       ret = ret.send(direction, width, pad_char)
295       ret[0] = plus_char unless plus_char.empty?
296     else
297       ret = plus_char + ret
298     end
299     ret
300   end
301   
302   def pad_char
303     flags[:zero] ? "0" : " "
304   end
305   
306   def plus_char
307     return "+" if flags[:plus]
308     return " " if flags[:space]
309     ""
310   end
311   
312   class Slot
313     
314     # pos means it got a N$ position
315     attr_reader :pos
316     attr_reader :position
317     attr_reader :value
318     attr_reader :str
319     
320     def initialize(str)
321       @pos = false
322       @str = str
323       if str.size == 3 && /\*\d\$/.match(str)
324         @pos = true
325         @position = str[1..1].to_i
326       elsif str.size == 2 && str[1] == ?$
327         @pos = true
328         @position = str[0..0].to_i
329       elsif str == "*"
330         @position = :next
331       else
332         @value = str.to_i
333       end
334     end
335   end
336   
337   class Number
338     
339     def initialize(number, radix)
340       @number = number
341       @radix = radix
342       @pad = (radix - 1).to_s(radix)
343     end
344     
345     def rep
346       return @number.to_s(@radix) if(@number >= 0) || @radix == 10
347       strlen = (@number.to_s(@radix)).size
348       max = (@pad * strlen).to_i(@radix)
349       ".." + (max + @number + 1).to_s(@radix)
350     end
351     
352   end
353