lxl 0.3.8 → 0.4.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 (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: