lxl 0.3.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/CHANGES +8 -0
  2. data/VERSION +1 -1
  3. data/lib/lxl.rb +114 -97
  4. data/test/lxl_test.rb +6 -6
  5. metadata +3 -3
data/CHANGES CHANGED
@@ -1,3 +1,11 @@
1
+ 0.4.0
2
+
3
+ - Tokenizing greatly refactored
4
+ - All tokenizing code moved into tokenize (out of eval)
5
+ - Token class added
6
+ - Fixes bug with text (non-formula) strings and constant parsing
7
+ - Removed unnessecary Lexer code
8
+
1
9
  0.3.8
2
10
 
3
11
  - Range#first_colnum and Range#last_colnum added (both use LXL#xlcolnum)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.8
1
+ 0.4.0
data/lib/lxl.rb CHANGED
@@ -35,7 +35,7 @@
35
35
  # NOW () # current date/time value
36
36
  # DATE (y,m,d) # date value
37
37
  # TIME (h,m,s) # time value
38
- # DATETIME (text) # convert a date/time string into a date/time value
38
+ # DATETIME (string) # convert a date/time string into a date/time value
39
39
  #
40
40
  # =List Functions
41
41
  #
@@ -76,7 +76,7 @@
76
76
  #
77
77
  # class MyLXLNamespace < LXL::Namespace
78
78
  # NAME = 'John Doe'
79
- # def upper(text) text.to_s.upcase end
79
+ # def upper(string) string.to_s.upcase end
80
80
  # end
81
81
  #
82
82
  # class MyLXL < LXL::Parser
@@ -86,7 +86,7 @@
86
86
  # MyLXL.eval('=UPPER(NAME)')
87
87
  # # => JOHN DOE
88
88
  #
89
- # =Symbol registration
89
+ # =Symbol Registration
90
90
  #
91
91
  # Register uppercase constants of the same name and value.
92
92
  #
@@ -96,7 +96,7 @@
96
96
  # LXL.new(MyNamespace.new).eval('=LIST(FOO, BAR)')
97
97
  # # => [:FOO, :BAR]
98
98
  #
99
- # =Deferred function calls
99
+ # =Deferred Calls
100
100
  #
101
101
  # LXL::Deferred snapshots the symbol/arguments of a function call for later use.
102
102
  #
@@ -154,7 +154,7 @@ module LXL
154
154
  def xlcolnum(colname)
155
155
  count = 0
156
156
  letters = colname.to_s.split(//)
157
- map = Proc.new { |c| ::Range.new('A','Z').collect.index(c)+1 }
157
+ map = proc { |c| ('A'..'Z').collect.index(c)+1 }
158
158
  letters[0..-2].each { |l| count += 26*map.call(l) }
159
159
  count += map.call(letters.last)
160
160
  count
@@ -166,6 +166,7 @@ end
166
166
  #
167
167
  # ; Statement separator
168
168
  # , Argument separator
169
+ # = Formula prefix
169
170
  # ( Tuple open
170
171
  # ) Tuple close
171
172
  # w Whitespace
@@ -175,13 +176,21 @@ end
175
176
  # p Percentage
176
177
  # f Float
177
178
  # i Integer
178
- # t Token
179
+ # u Unknown
179
180
  #
180
181
  # F Function
181
182
  # C Constant
182
183
  #
183
184
  class LXL::Parser
184
185
 
186
+ class Token
187
+ attr_accessor :type, :value
188
+ def initialize(type, value) @type = type; @value = value end
189
+ def whitespace?() value.to_s.strip.empty? end
190
+ def to_s() "T:#{@type.chr}:#{@value}" end
191
+ alias inspect to_s
192
+ end
193
+
185
194
  RUBY_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '==', '!=', '<', '>', '+', '**']
186
195
  EXCEL_OPERATORS = ['+', '-', '*', '/', '<=', '>=', '=', '<>', '<', '>', '&', '^' ]
187
196
 
@@ -218,6 +227,7 @@ class LXL::Parser
218
227
  @lexer = self.class.lexer_class.new([
219
228
  [/\A;+/, ?;], # Statement separator
220
229
  [/\A,+/, ?,], # Argument separator
230
+ [/\A`=/, ?=], # Formula prefix
221
231
  [/\A\(/, ?(], # Tuple open
222
232
  [/\A\)/, ?)], # Tuple close
223
233
  [/\A\s+/, ?w], # Whitespace
@@ -227,29 +237,15 @@ class LXL::Parser
227
237
  [/\A\d+(\.\d+)?%/, ?p], # Percentage
228
238
  [/\A\d+\.\d+/, ?f], # Float
229
239
  [/\A\d+/, ?i], # Integer
230
- [/\A\w+/, ?t], # Token
231
- ], false)
240
+ [/\A\w+/, ?u], # Unknown
241
+ ])
232
242
  end
233
243
 
234
244
  # Evaluate a formula.
235
245
  #
236
- def eval(formula)
237
- formulas = Array.new << Array.new
238
- formula = formula.to_s.gsub(/(\n)(\s*=)/,'\1;\2') # infer statement separators
239
- types,tokens = tokenize(formula)
240
- tokens.each_index { |i| tokens[i] == ';' ? formulas << Array.new : formulas.last << [types[i], tokens[i]] }
241
- formulas.collect! { |f|
242
- types = f.collect { |t| t.first }
243
- tokens = f.collect { |t| t.last }
244
- token = tokens.join.strip
245
- if token =~ /\A=/
246
- tokens.each_index { |i| tokens[i] = translate_quotes(tokens[i]) if types[i] == ?s }
247
- token = tokens.join.strip.gsub(/\A=+/,'')
248
- else
249
- token = translate_quotes(quote(tokens.join.strip))
250
- end
251
- }
252
- formulas.delete_if { |f| f == '""' }
246
+ def eval(string)
247
+ formulas = tokenize(string)
248
+ formulas.collect! { |f| f.collect { |t| t.value }.join }
253
249
  formulas.collect! { |f| Kernel.eval(f, binding) }
254
250
  formulas.size == 1 ? formulas.first : formulas
255
251
  end
@@ -261,8 +257,8 @@ class LXL::Parser
261
257
  # quote('a "quoted" value.')
262
258
  # # => '"a ""quoted"" value."'
263
259
  #
264
- def quote(text)
265
- '"'+text.to_s.gsub('"','""')+'"'
260
+ def quote(string)
261
+ '"'+string.to_s.gsub('"','""')+'"'
266
262
  end
267
263
 
268
264
  # Translate "" to \" in quoted strings.
@@ -270,58 +266,91 @@ class LXL::Parser
270
266
  # translate_quotes('"a ""quoted"" value."')
271
267
  # # => '"a \"quoted\" value."'
272
268
  #
273
- def translate_quotes(text)
274
- text.to_s.gsub(/([^\\])""/,'\1\"')
269
+ def translate_quotes(string)
270
+ string.to_s.gsub(/([^\\])""/,'\1\"')
275
271
  end
276
272
 
277
- # Tokenize a formula into <tt>[TypesString, TokensArray]</tt>.
273
+ # Tokenize a string into an array of formulas (Array of Token).
278
274
  #--
279
275
  # const_missing is defined by Ruby, raises a NameError.
280
276
  # method_missing is not, Ruby uses user-defined if present, NoMethodError otherwise.
281
277
  #++
282
- def tokenize(formula)
278
+ def tokenize(string)
279
+ formulas = [[]]
280
+ separators = [?=, ?;]
283
281
  ops = Hash[*EXCEL_OPERATORS.zip(RUBY_OPERATORS).flatten]
284
282
 
285
- # Parse formula
286
- types,tokens = @lexer.scan(formula.to_s)
283
+ # Translate formula prefixes to `= to avoid clashing with the = operator
284
+ # For lines that start with = (optional leading whitespace)
285
+ string.gsub!(/^\s*=/, '`=')
286
+
287
+ # Parse string
288
+ types,tokens = @lexer.scan(string.to_s)
287
289
  raise SyntaxError, 'unbalanced parentheses' unless balanced?(types)
288
290
 
291
+ # Translate formula prefixes to `= to avoid clashing with the = operator
292
+ # For single line separations via semi-colon: semi-colon (optional whitespace) =
293
+ types.each_index do |i|
294
+ type,token = types[i], tokens[i]
295
+ if token == '='
296
+ last = types[0..i-1].reverse.find { |t| t != ?w }
297
+ types[i] = ?= if last == ?;
298
+ end
299
+ end
300
+
289
301
  # Parse tokens
290
- tokens.each_index do |i|
291
- type, token = types[i], tokens[i]
292
- tokens[i] = case type
293
- when ?o then ops[token]
294
- when ?p then token.to_f/100
295
- when ?f then token.to_f
296
- when ?i then token.to_i
297
- when ?r then
298
- range = token.split(/:/)
299
- range[1] = range[0] if range.size == 1
300
- "self.class.range_class.new(*#{range.inspect})"
301
- when ?t then
302
- upper = token.to_s.upcase
303
- lower = token.to_s.downcase
304
- raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
305
- custom_const_missing = begin [@namespace.const_get(:ConstMissing),true].last rescue false end
306
- # Constants
307
- if tokens[i+1] != '('
308
- if @namespace.const_defined?(upper) or custom_const_missing
309
- types[i] = ?C
310
- "@namespace.const_get(:#{upper})"
311
- else token; end
312
- # Functions
313
- else
314
- if @namespace.respond_to?(lower) or @namespace.respond_to?(:method_missing)
315
- types[i] = ?F
316
- "@namespace.#{lower}"
317
- else token; end
318
- end
319
- else token; end
302
+ last_type = ??
303
+ while tokens.size > 0
304
+ type,token = types.shift, tokens.shift
305
+ # Text
306
+ if formulas.last.size == 0 and last_type != ?= and type != ?=
307
+ text = []
308
+ while tokens.size > 0 and ! separators.include?(types.first)
309
+ types.shift
310
+ text << tokens.shift
311
+ end
312
+ text = text.join.chomp
313
+ formulas.last << Token.new(type, translate_quotes(quote(text))) unless text.empty?
314
+ # Formula
315
+ else
316
+ t = Token.new(type, token)
317
+ case type
318
+ when ?; then formulas << []
319
+ when ?= then formulas << []
320
+ when ?o then formulas.last << Token.new(type, ops[token])
321
+ when ?s then formulas.last << Token.new(type, translate_quotes(token))
322
+ when ?p then formulas.last << Token.new(type, token.to_f/100)
323
+ when ?f then formulas.last << Token.new(type, token.to_f)
324
+ when ?i then formulas.last << Token.new(type, token.to_i)
325
+ when ?r then
326
+ range = token.split(/:/)
327
+ range[1] = range[0] if range.size == 1
328
+ formulas.last << Token.new(type, "self.class.range_class.new(*#{range.inspect})")
329
+ when ?u then
330
+ upper = token.to_s.upcase
331
+ lower = token.to_s.downcase
332
+ raise NoMethodError, "protected method `#{token}` called for #{self}" if @namespace.const_get(:METHODS).include?(lower)
333
+ custom_const_missing = begin [@namespace.const_get(:ConstMissing),true].last rescue false end
334
+ # Constants
335
+ if tokens.first != '('
336
+ if @namespace.const_defined?(upper) or custom_const_missing
337
+ formulas.last << Token.new(?C, "@namespace.const_get(:#{upper})")
338
+ else formulas.last << t end
339
+ # Functions
340
+ else
341
+ if @namespace.respond_to?(lower) or @namespace.respond_to?(:method_missing)
342
+ formulas.last << Token.new(?F, "@namespace.#{lower}")
343
+ else formulas.last << t end
344
+ end
345
+ else formulas.last << t end
346
+ end
347
+ last_type = type
320
348
  end
321
349
 
322
- [types,tokens]
350
+ formulas.reject! { |f| f.size == 0 }
351
+ formulas
323
352
  end
324
-
353
+
325
354
  # Check that parentheses are balanced.
326
355
  #
327
356
  def balanced?(list)
@@ -352,7 +381,6 @@ end
352
381
  #
353
382
  class LXL::Range < Range
354
383
 
355
- # B3 | B3: | B3:D5 | Sheet1!B3:D5 | [Book1]Sheet1!B3:D5 | [file.xls]Sheet1!B3:D5
356
384
  EXCEL_RANGE = /\A(\[([\w\.]+)\])?((\w+)!)?([A-Z]+[1-9]+)(:([A-Z]+[1-9]+)?)?/i
357
385
 
358
386
  # Workbook name.
@@ -410,8 +438,8 @@ class LXL::Range < Range
410
438
 
411
439
  def each
412
440
  if excel?
413
- Range.new(first_column, last_column).each do |column|
414
- Range.new(first_cell, last_cell).each do |cell|
441
+ (first_column..last_column).each do |column|
442
+ (first_cell..last_cell).each do |cell|
415
443
  yield column+cell.to_s
416
444
  end
417
445
  end
@@ -498,7 +526,7 @@ end
498
526
  #
499
527
  # class MyLXLNamespace < LXL::Namespace
500
528
  # NAME = 'John Doe'
501
- # def upper(text) text.to_s.upcase end
529
+ # def upper(string) string.to_s.upcase end
502
530
  # end
503
531
  #
504
532
  # class MyLXL < LXL::Parser
@@ -550,8 +578,8 @@ class LXL::Namespace < LXL::EmptyNamespace
550
578
  Date.new(y,m,d).ajd.to_f
551
579
  end
552
580
 
553
- def datetime(text)
554
- DateTime.parse(text.to_s).ajd.to_f
581
+ def datetime(string)
582
+ DateTime.parse(string.to_s).ajd.to_f
555
583
  end
556
584
 
557
585
  def time(h,m,s)
@@ -562,35 +590,24 @@ end
562
590
 
563
591
  # Based on John Carter's LittleLexer (http://littlelexer.rubyforge.org).
564
592
  #
565
- class LXL::Lexer #:nodoc: all
593
+ class LXL::Lexer
566
594
 
567
595
  class LexerJammed < Exception; end
568
596
 
569
- def initialize(re_to_chr, skip_whitespace=true)
597
+ def initialize(re_to_chr)
570
598
  @re_to_chr = re_to_chr
571
- @skip_whitespace = skip_whitespace
572
- end
573
-
574
- def scan(string, string_tokens=nil)
575
- types = String.new
576
- tokens = Array.new
577
- if string_tokens
578
- next_token(string) do |t,token,tail|
579
- types << t
580
- tokens << [string_tokens[0...tail], string[0...tail]]
581
- string = string[tail..-1]
582
- string_tokens = string_tokens[tail..-1]
583
- end
584
- else
585
- next_token(string) do |t,token,tail|
586
- types << t
587
- tokens << token
588
- end
599
+ end
600
+
601
+ def scan(string)
602
+ types,tokens = [],[]
603
+ next_token(string) do |type,token|
604
+ types << type
605
+ tokens << token
589
606
  end
590
- return types,tokens
607
+ [types,tokens]
591
608
  end
592
609
 
593
- private
610
+ protected
594
611
 
595
612
  def next_token(string)
596
613
  match_data = nil
@@ -600,7 +617,7 @@ class LXL::Lexer #:nodoc: all
600
617
  match_data = re.match(string)
601
618
  next unless match_data
602
619
  token = match_data[0]
603
- yield chr,token, match_data.end(0) unless chr == ?\s && @skip_whitespace
620
+ yield chr, token
604
621
  string = match_data.post_match
605
622
  failed = false
606
623
  break
@@ -618,8 +635,8 @@ end
618
635
  if $0 == __FILE__
619
636
 
620
637
  formulas = %{
621
- This is some text;
622
- ="This is some ""quoted"" text"
638
+ This is a string;
639
+ ="This is a ""quoted"" string"
623
640
  =((1+2)*(10-6))/2
624
641
  =datetime("2004-11-22 11:11:00")=DATE(2004,11,22)+TIME(11,11,00)
625
642
  =IN(" is ", "this is a string")
@@ -638,6 +655,6 @@ if $0 == __FILE__
638
655
 
639
656
  # multiple formulas separated by semi-colon
640
657
  puts LXL.eval(formulas).inspect
641
- # => ["This is some text", "This is some \"quoted\" text", 6, true, true, [1, "two", 3.0], true, false, true, "yes", "this and that", 8, 0.502]
658
+ # => ["This is a string", "This is a \"quoted\" string", 6, true, true, [1, "two", 3.0], true, false, true, "yes", "this and that", 8, 0.502]
642
659
 
643
660
  end
@@ -5,7 +5,7 @@ require 'lxl'
5
5
  class MyNamespace < LXL::Namespace
6
6
  register_symbols :foo
7
7
  register_deferred :bar
8
- def capitalize(text) text.to_s.capitalize end
8
+ def capitalize(string) string.to_s.capitalize end
9
9
  end
10
10
 
11
11
  class LXLTest < Test::Unit::TestCase
@@ -16,9 +16,9 @@ class LXLTest < Test::Unit::TestCase
16
16
 
17
17
  def test_multiple_formula
18
18
  formulas = %{
19
- This is some text;
20
- "This is some ""quoted"" text"
21
- ="This is some ""quoted"" text"
19
+ This is a string;
20
+ "This is a ""quoted"" string"
21
+ ="This is a ""quoted"" string"
22
22
  =";"=";"
23
23
  =";"=":"
24
24
  =A
@@ -39,7 +39,7 @@ class LXLTest < Test::Unit::TestCase
39
39
  =0.2589=25.89%
40
40
  ="embedded percentages 25% and semi-colons ; are working properly"
41
41
  }
42
- expected = ["This is some text", "\"This is some \"quoted\" text\"", "This is some \"quoted\" text"]
42
+ expected = ["This is a string", "\"This is a \"quoted\" string\"", "This is a \"quoted\" string"]
43
43
  expected += [true, false, :A, :B, false, 6, true, true, [1, 'two', 3.0], true, false, true, 'yes', 'this and that']
44
44
  expected += [8, 0.502, true, true, 'embedded percentages 25% and semi-colons ; are working properly']
45
45
  MyNamespace.register_symbols(:A, :B)
@@ -132,7 +132,7 @@ class LXLTest < Test::Unit::TestCase
132
132
 
133
133
  class MyLXLNamespace < LXL::Namespace
134
134
  NAME = 'John Doe'
135
- def upper(text) text.to_s.upcase end
135
+ def upper(string) string.to_s.upcase end
136
136
  end
137
137
 
138
138
  class MyLXL < LXL::Parser
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.3
3
3
  specification_version: 1
4
4
  name: lxl
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.8
7
- date: 2005-02-15
6
+ version: 0.4.0
7
+ date: 2005-02-16
8
8
  summary: LXL (Like Excel) is a mini-language that mimics Microsoft Excel formulas. Easily extended with new constants and functions.
9
9
  require_paths:
10
10
  - lib
@@ -36,8 +36,8 @@ files:
36
36
  - README.en
37
37
  - lib/lxl.rb
38
38
  - test/spreadsheet.rb
39
- - test/lxl_test.rb
40
39
  - test/lxl_spreadsheet_test.rb
40
+ - test/lxl_test.rb
41
41
  test_files:
42
42
  - test/lxl_test.rb
43
43
  rdoc_options: