sdl4r 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,3 @@
1
+ == sdl4r
2
+
3
+
@@ -0,0 +1,45 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require 'rubygems'
7
+
8
+ spec = Gem::Specification.new do |s|
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = "Simple Declarative Language for Ruby library"
11
+ s.name = 'sdl4r'
12
+ s.version = '0.9.1'
13
+ s.requirements << 'none'
14
+ s.require_path = 'lib'
15
+ s.author = 'Philippe Vosges'
16
+ s.email = 'sdl-users@ikayzo.org'
17
+ s.rubyforge_project = 'sdl4r'
18
+ s.homepage = 'http://www.ikayzo.org/confluence/display/SDL/Home'
19
+ s.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
20
+ s.test_files = FileList[ 'test/**/*test.rb' ].to_a
21
+ s.description = <<EOF
22
+ The Simple Declarative Language provides an easy way to describe lists, maps,
23
+ and trees of typed data in a compact, easy to read representation.
24
+ For property files, configuration files, logs, and simple serialization
25
+ requirements, SDL provides a compelling alternative to XML and Properties
26
+ files.
27
+ EOF
28
+ end
29
+
30
+ Rake::GemPackageTask.new(spec) do |pkg|
31
+ pkg.need_zip = true
32
+ pkg.need_tar = true
33
+ end
34
+
35
+ Rake::RDocTask.new do |rd|
36
+ rd.rdoc_files.include("lib/**/*.rb")
37
+ rd.rdoc_dir = "doc"
38
+ rd.title = "Simple Declarative Language for Ruby"
39
+ end
40
+
41
+ Rake::TestTask.new do |t|
42
+ t.libs << "lib"
43
+ t.test_files = FileList['test/**/*test.rb']
44
+ t.verbose = true
45
+ end
@@ -0,0 +1,117 @@
1
+ [x] Tag.each_attribute should allow to iterate on all namespaces if 'namespace' is nil
2
+ [x] Tag.get_attributes(namespace)
3
+ [x] Implementation of attributes=
4
+ [x] Implementation of Sdl.format()
5
+ [x] Handle milliseconds in Sdl.format()
6
+ [x] Handle timezones in Sdl.format()
7
+ [x] Handle the Time class in Sdl.format()
8
+ [x] Handle SdlTimeSpan in Sdl.format() ==> via to_s()
9
+ [x] Review SdlTimeSpan
10
+ [x] Instead of calling Tag.each_attribute(), we could have another way of calling attributes:
11
+ with or without given block. Same goes for each_value(), etc.
12
+ [x] Is Base64 really compatible with the format defined in the Java version ?
13
+ ==> Seems so after having implemented more of the standard tests.
14
+ [ ] Support both DateTime and Time in parsing (see Parser#combine())
15
+ [x] Use Date instead of DateTime if only day was specified in SDL (parser.rb)
16
+ [x] Add a remove_all_children() to Tag
17
+ [x] Rethink the interfaces to access sub-tags, values, attributes, etc
18
+ (it is currently difficult to retrieve all the values, or the number of
19
+ sub-tags, etc)
20
+ ==> to simplify access to values would be ok but attributes or children would be more difficult.
21
+ [x] See whether there is such a need to shield users from the actual arrays of
22
+ values (we return a copy for the time being).
23
+ ==> We don't return copies anymore.
24
+ [ ] Add more unit tests
25
+ [x] Attribute tests
26
+ [x] Date tests
27
+ [x] Date + time test
28
+ [x] Time zone tests
29
+ [x] Number literal tests
30
+ [ ] Strings literals (especially with line continuations)
31
+ [ ] Sub tags tests
32
+ [x] "null" value test
33
+ [x] Comment tests
34
+ [ ] Bad syntax tests
35
+ [ ] Test write (unit tests)
36
+ [ ] Dates
37
+ [ ] Numbers
38
+ [ ] Use YARD in order to generate documentation ?
39
+ [ ] In the documentation, present a table giving the returned Ruby type for each SDL type.
40
+ [A] Change the interface of SdlTimeSpan to look like the interfaces of Date, DateTime or Time
41
+ ==> Really? This is a timespan, not a date.
42
+ [x] Have SdlTimeSpan implement Comparable
43
+ [ ] BUG: the line number is too high by 1 (the column is correct).
44
+ [x] PB: binary fields shouldn't be kept as Strings because they would not be saved as binaries but
45
+ as strings otherwise.
46
+ ==> We use SdlBinary now.
47
+ [x] Change the module name to SDL4R? Something else?
48
+ ==> Changed to SDL4R.
49
+ [/] Fix the differences between test_basic_types.sdl and what is generated from the parsed structure
50
+ [x] chars
51
+ [x] longs
52
+ [x] doubles
53
+ [x] decimals
54
+ [x] booleans
55
+ [x] null
56
+ [x] dates
57
+ [x] times
58
+ [x] negative times
59
+ [x] datetimes
60
+ [/] zone codes
61
+ ==> Time only works in UTC, which means that the original zone code is lost.
62
+ ==> DateTime doesn't give the zone code but only the offset.
63
+ [ ] Use TzTime? Use a custom object that knows whether a time zone was specified?
64
+ [x] BUG: Base64 wrapped lines are 64 chars long and not 72 (traditionnal length)
65
+ ==> Because of a limitation in the regular expressions ==> find another algorithm
66
+ [x] See whether we can do better for numbers of arbitrary precision (decimals).
67
+ ==> Ruby BigDecimal
68
+ ==> Ruby Decimal http://ruby-decimal.rubyforge.org/
69
+ ==> Now: http://flt.rubyforge.org/ : use this if the lib is available
70
+ ==> We use flt if available.
71
+ [ ] See how Ruby floats relate to Java floats and doubles.
72
+ [ ] Add tests for the SDL class
73
+ [ ] Allow unicode characters in identifiers.
74
+ [ ] It would probably be useful to allow people to replace the standard types by their own. This
75
+ could be useful for dates or numbers, for instance.
76
+ [N] To install a gem in the Netbeans gems repository, it needs to run as an administrator.
77
+ Otherwise, it fails silently.
78
+ [ ] Make it so that the tests pass (with errors or not), when Ftl (DecNum) is not available.
79
+ [x] Fix the ParserTest test.
80
+ [x] SDL 1.1: tag1; tag2 "a value"; tag3 name="foo"
81
+ [x] Create a Tokenizer class
82
+ [A] Test attributes with/without namespaces
83
+ ==> Guess this is done in test_structures...
84
+ [ ] Propose to Dan that the top level is considered as a root tag that can't have values but
85
+ just attributes and sub-tags.
86
+ [x] Fix Test.test_strings: support for Unicode
87
+ ==> Seems to work.
88
+ [ ] Consider being able to read text files that are not UTF-8(?)
89
+ [ ] BUG: the report on the line no in errors is off by 1 (at least in some cases)
90
+ [ ] Try to move Reader, Tokenizer, etc to the private part of the SDL module
91
+ ==> Doesn't seem to make a difference.
92
+ [x] Factorize "each_child..." methods in Tag and refactor "children(recursive, name)" consequently
93
+ + invert "name" and "recursive" in "children()"
94
+ [ ] Return copies or original arrays in Tag?
95
+ [ ] BUG: test_tag_write_parse() does not work from the command line (ruby v1.8.7).
96
+ [ ] Tag.to_string(): break up long lines using the backslash
97
+ [ ] Tag.hash: the implementation is not very efficient.
98
+ [ ] Implement reading from a URL(?) Other sources idiomatic in Ruby?
99
+ [ ] See the XML and YAML APIs to find enhancements.
100
+ [ ] Check the XML functionalities of Tag.
101
+ [ ] Add tests for Tag
102
+ [ ] Maybe some methods in the SDL module are not so useful to the general user: make them protected?
103
+ [ ] FUTURE: xpath, ypath ==> sdlpath(?)
104
+ [ ] FUTURE: evenemential parsing(?)
105
+ [ ] Move Tokenizer, Reader, etc into the Parser module/class or prefix by Sdl
106
+ [ ] FUTURE: add a way to insert a tag after or before another(?)
107
+ [ ] FUTURE: allow some way of generating YAML(?)
108
+ [ ] FUTURE: allow to turn a YAML structure into a SDL one(?)
109
+ [ ] Make a Gem
110
+ [ ] Implement the convenience method of SDL (value(), list(), map())
111
+ [x] Move the test files to a sdl4r subdir
112
+ [ ] 1.2: Ensure that there is a SDL.read() that returns a Tag
113
+ [ ] 1.2: hasChild(), hasChildren(), getChildMap(), getChildStringMap() methods
114
+ [ ] 1.3: periods in identifiers
115
+ [ ] Create a History.txt file
116
+ [ ] Setup the Rubyforge website
117
+ [ ] See how both Subversion repositories can be handled (is it necessary?)
@@ -0,0 +1,49 @@
1
+ if RUBY_VERSION < '1.9.0'
2
+ $KCODE = 'u'
3
+ require 'jcode'
4
+ end
5
+
6
+ #require "rubygems"
7
+ require "flt"
8
+ #
9
+ #puts "DecNum=" + Flt::DecNum("12345678901234567890").to_s
10
+ #puts "DecNum precision=" + Flt::DecNum.context.precision.to_s
11
+
12
+ puts "Flt::DecNum available" if defined? Flt::DecNum
13
+
14
+ require 'time'
15
+ require File.dirname(__FILE__) + '/sdl4r/sdl'
16
+ require File.dirname(__FILE__) + '/sdl4r/tag'
17
+
18
+ #if "+09:00" =~ /(?:-([a-zA-Z]+))|(?:([\+\-]\d+)(?::(\d+))?)/
19
+ # puts "matches " + $1.to_s + " " + $2.to_s
20
+ #end
21
+
22
+ #if "03:00-UTC-04" =~ /^([+-]?\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?(?:(?:-([a-zA-Z]+))?(?:([\+\-]\d+)(?::(\d+))?)?)?$/i
23
+ # puts $~
24
+ #end
25
+
26
+ root = SDL4R::Tag.new("root")
27
+ #open("D:\\dev\\sdl\\sdl4r\\test\\test_structures.sdl") do |io|
28
+ # root.read(io)
29
+ #end
30
+ ##root.read(
31
+ ##<<EOF
32
+ ##matrix {
33
+ # 1 2 3
34
+ # 4 5 6
35
+ #}
36
+ ##EOF
37
+ ##)
38
+ root.read(
39
+ <<EOF
40
+ toto titi=null tata=2
41
+ EOF
42
+ )
43
+ #local_offset = DateTime.now.offset
44
+ #puts "local_offset=#{local_offset * 24}"
45
+ #puts DateTime.civil(1980,12,5,12,30,0,local_offset)
46
+
47
+ root.children { |child| puts child.to_s }
48
+
49
+ puts root.to_s
@@ -0,0 +1,678 @@
1
+ # Simple Declarative Language (SDL) for Ruby
2
+ # Copyright 2005 Ikayzo, inc.
3
+ #
4
+ # This program is free software. You can distribute or modify it under the
5
+ # terms of the GNU Lesser General Public License version 2.1 as published by
6
+ # the Free Software Foundation.
7
+ #
8
+ # This program is distributed AS IS and WITHOUT WARRANTY. OF ANY KIND,
9
+ # INCLUDING MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
10
+ # See the GNU Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public License
13
+ # along with this program; if not, contact the Free Software Foundation, Inc.,
14
+ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15
+
16
+
17
+ module SDL4R
18
+
19
+ require 'base64'
20
+
21
+ begin
22
+ # Try to use the Flt library, which defines DecNum
23
+ require "flt"
24
+ rescue LoadError
25
+ # Well, shouganai.
26
+ end
27
+
28
+ require File.dirname(__FILE__) + '/sdl_binary'
29
+ require File.dirname(__FILE__) + '/sdl_time_span'
30
+ require File.dirname(__FILE__) + '/sdl_parse_error'
31
+ require File.dirname(__FILE__) + '/tokenizer'
32
+
33
+ # The SDL parser.
34
+ #
35
+ # Authors: Daniel Leuck, Philippe Vosges
36
+ #
37
+ # In Ruby 1.8, in order to enable UTF-8 support, you may have to declare the following lines:
38
+ #
39
+ # $KCODE = 'u'
40
+ # require 'jcode'
41
+ #
42
+ # This will give you correct input and output and correct UTF-8 "general" sorting.
43
+ # Alternatively you can use the following options when launching the Ruby interpreter:
44
+ #
45
+ # /path/to/ruby -Ku -rjcode
46
+ #
47
+ class Parser
48
+
49
+ # Passed to parse_error() in order to specify an error that occured on no specific position
50
+ # (column).
51
+ UNKNOWN_POSITION = -2
52
+
53
+ # Creates an SDL parser on the specified +IO+.
54
+ def initialize(io)
55
+ raise ArgumentError, "io == nil" if io.nil?
56
+
57
+ @tokenizer = Tokenizer.new(io)
58
+ end
59
+
60
+ # Parses the underlying +IO+ and returns an +Array+ of +Tag+.
61
+ #
62
+ # ==Errors
63
+ # [IOError] If a problem is encountered with the IO
64
+ # [SdlParseError] If the document is malformed
65
+ def parse
66
+ tags = []
67
+
68
+ while tokens = @tokenizer.read_line_tokens()
69
+ if tokens.last.type == :START_BLOCK
70
+ # tag with a block
71
+ tag = construct_tag(tokens[0...-1])
72
+ add_children(tag)
73
+ tags << tag
74
+
75
+ elsif tokens.first.type == :END_BLOCK
76
+ # we found an block end token that should have been consumed by
77
+ # add_children() normally
78
+ parse_error(
79
+ "No opening block ({) for close block (}).",
80
+ tokens.first.line,
81
+ tokens.first.position)
82
+ else
83
+ # tag without block
84
+ tags << construct_tag(tokens)
85
+ end
86
+ end
87
+
88
+ @tokenizer.close()
89
+
90
+ return tags
91
+ end
92
+
93
+ private
94
+
95
+ # Parses the children tags of +parent+ until an end of block is found.
96
+ def add_children(parent)
97
+ while tokens = @tokenizer.read_line_tokens()
98
+ if tokens.first.type == :END_BLOCK
99
+ return
100
+
101
+ elsif tokens.last.type == :START_BLOCK
102
+ # found a child with a block
103
+ tag = construct_tag(tokens[0...-1]);
104
+ add_children(tag)
105
+ parent.add_child(tag)
106
+
107
+ else
108
+ parent.add_child(construct_tag(tokens))
109
+ end
110
+ end
111
+
112
+ parse_error("No close block (}).", @tokenizer.line_no, UNKNOWN_POSITION)
113
+ end
114
+
115
+ # Construct a Tag (but not its children) from a string of tokens
116
+ #
117
+ # Throws SdlParseError if some bad syntax is found.
118
+ def construct_tag(tokens)
119
+ raise ArgumentError, "tokens == nil" if tokens.nil?
120
+ if tokens.empty?
121
+ parse_error("Internal Error: empty token list", @tokenizer.line_no, UNKNOWN_POSITION)
122
+ end
123
+
124
+ first_token = tokens.first
125
+ if first_token.literal?
126
+ first_token = Token.new("content")
127
+ tokens.insert(0, first_token)
128
+
129
+ elsif first_token.type != :IDENTIFIER
130
+ expecting_but_got(
131
+ "IDENTIFIER",
132
+ "#{first_token.type} (#{first_token.text})",
133
+ first_token.line,
134
+ first_token.position)
135
+ end
136
+
137
+ tag = nil
138
+ if tokens.size == 1
139
+ tag = Tag.new(first_token.text)
140
+
141
+ else
142
+ values_start_index = 1
143
+ second_token = tokens[1]
144
+
145
+ if second_token.type == :COLON
146
+ if tokens.size == 2 or tokens[2].type != :IDENTIFIER
147
+ parse_error(
148
+ "Colon (:) encountered in unexpected location.",
149
+ second_token.line,
150
+ second_token.position)
151
+ end
152
+
153
+ third_token = tokens[2];
154
+ tag = Tag.new(third_token.text, first_token.text)
155
+ values_start_index = 3
156
+
157
+ else
158
+ tag = Tag.new(first_token.text)
159
+ end
160
+
161
+ # read values
162
+ attribute_start_index = add_tag_values(tag, tokens, values_start_index)
163
+
164
+ # read attributes
165
+ if attribute_start_index < tokens.size
166
+ add_tag_attributes(tag, tokens, attribute_start_index)
167
+ end
168
+ end
169
+
170
+ return tag
171
+ end
172
+
173
+ #
174
+ # @return The position at the end of the value list
175
+ #
176
+ def add_tag_values(tag, tokens, start)
177
+ size = tokens.size()
178
+ i = start;
179
+
180
+ while i < size
181
+ token = tokens[i]
182
+
183
+ if token.literal?
184
+ # if a DATE token is followed by a TIME token combine them
185
+ next_token = ((i + 1) < size)? tokens[i + 1] : nil
186
+ if token.type == :DATE && next_token && next_token.type == :TIME
187
+ date = token.object_for_literal()
188
+ time_zone_with_zone = next_token.object_for_literal()
189
+
190
+ if time_zone_with_zone.day != 0
191
+ # as there are days specified, it can't be a full precision date
192
+ tag.add_value(date);
193
+ tag.add_value(
194
+ SdlTimeSpan.new(
195
+ time_zone_with_zone.day,
196
+ time_zone_with_zone.hour,
197
+ time_zone_with_zone.min,
198
+ time_zone_with_zone.sec))
199
+
200
+
201
+ if time_zone_with_zone.time_zone_offset
202
+ parse_error("TimeSpan cannot have a timeZone", t.line, t.position)
203
+ end
204
+
205
+ else
206
+ tag.add_value(combine(date, time_zone_with_zone))
207
+ end
208
+
209
+ i += 1
210
+
211
+ else
212
+ value = token.object_for_literal()
213
+ if value.is_a?(TimeSpanWithZone)
214
+ # the literal looks like a time zone
215
+ if value.time_zone_offset
216
+ expecting_but_got(
217
+ "TIME SPAN",
218
+ "TIME (component of date/time)",
219
+ token.line,
220
+ token.position)
221
+ end
222
+
223
+ tag.add_value(
224
+ SdlTimeSpan.new(
225
+ value.day,
226
+ value.hour,
227
+ value.min,
228
+ value.sec))
229
+ else
230
+ tag.add_value(value)
231
+ end
232
+ end
233
+ elsif token.type == :IDENTIFIER
234
+ break
235
+ else
236
+ expecting_but_got(
237
+ "LITERAL or IDENTIFIER", token.type, token.line, token.position)
238
+ end
239
+
240
+ i += 1
241
+ end
242
+
243
+ return i
244
+ end
245
+
246
+ #
247
+ # Add attributes to the given tag
248
+ #
249
+ def add_tag_attributes(tag, tokens, start)
250
+ i = start
251
+ size = tokens.size
252
+
253
+ while i < size
254
+ token = tokens[i]
255
+ if token.type != :IDENTIFIER
256
+ expecting_but_got("IDENTIFIER", token.type, token.line, token.position)
257
+ end
258
+ name_or_namespace = token.text;
259
+
260
+ if i == (size - 1)
261
+ expecting_but_got(
262
+ "\":\" or \"=\" \"LITERAL\"",
263
+ "END OF LINE.",
264
+ token.line,
265
+ token.position)
266
+ end
267
+
268
+ i += 1
269
+ token = tokens[i]
270
+ if token.type == :COLON
271
+ if i == (size - 1)
272
+ expecting_but_got(
273
+ "IDENTIFIER", "END OF LINE", token.line, token.position)
274
+ end
275
+
276
+ i += 1
277
+ token = tokens[i]
278
+ if token.type != :IDENTIFIER
279
+ expecting_but_got(
280
+ "IDENTIFIER", token.type, token.line, token.position)
281
+ end
282
+ name = token.text
283
+
284
+ if i == (size - 1)
285
+ expecting_but_got("\"=\"", "END OF LINE", token.line, token.position)
286
+ end
287
+
288
+ i += 1
289
+ token = tokens[i]
290
+ if token.type != :EQUALS
291
+ expecting_but_got("\"=\"", token.type, token.line, token.position)
292
+ end
293
+
294
+ if i == (size - 1)
295
+ expecting_but_got("LITERAL", "END OF LINE", token.line, token.position)
296
+ end
297
+
298
+ i += 1
299
+ token = tokens[i]
300
+ if !token.literal?
301
+ expecting_but_got("LITERAL", token.type, token.line, token.position)
302
+ end
303
+
304
+ if token.type == :DATE and (i + 1) < size and tokens[i + 1].type == :TIME
305
+ date = token.get_object_for_literal()
306
+ time_span_with_zone = tokens[i + 1].get_object_for_literal()
307
+
308
+ if time_span_with_zone.days != 0
309
+ expecting_but_got(
310
+ "TIME (component of date/time) in attribute value",
311
+ "TIME SPAN",
312
+ token.line,
313
+ token.position)
314
+ else
315
+ tag.set_attribute(
316
+ name, combine(date, time_span_with_zone), name_or_namespace)
317
+ end
318
+
319
+ i += 1
320
+ else
321
+ value = token.object_for_literal();
322
+ if value.is_a?(TimeSpanWithZone)
323
+ time_span_with_zone = value
324
+
325
+ if time_span_with_zone.time_zone_offset
326
+ expecting_but_got(
327
+ "TIME SPAN",
328
+ "TIME (component of date/time)",
329
+ token.line,
330
+ token.position)
331
+ end
332
+
333
+ time_span = SdlTimeSpan.new(
334
+ time_span_with_zone.day,
335
+ time_span_with_zone.hour,
336
+ time_span_with_zone.min,
337
+ time_span_with_zone.sec)
338
+
339
+ tag.set_attribute(name, time_span, name_or_namespace)
340
+ else
341
+ tag.set_attribute(name, value, name_or_namespace);
342
+ end
343
+ end
344
+ elsif token.type == :EQUALS
345
+ if i == (size - 1)
346
+ expecting_but_got("LITERAL", "END OF LINE", token.line, token.position)
347
+ end
348
+
349
+ i += 1
350
+ token = tokens[i]
351
+ if !token.literal?
352
+ expecting_but_got("LITERAL", token.type, token.line, token.position)
353
+ end
354
+
355
+ if token.type == :DATE and (i + 1) < size and tokens[i + 1].type == :TIME
356
+ date = token.object_for_literal()
357
+ time_span_with_zone = tokens[i + 1].object_for_literal()
358
+
359
+ if time_span_with_zone.day != 0
360
+ expecting_but_got(
361
+ "TIME (component of date/time) in attribute value",
362
+ "TIME SPAN",
363
+ token.line,
364
+ token.position)
365
+ end
366
+ tag.set_attribute(
367
+ name_or_namespace, combine(date, time_span_with_zone))
368
+
369
+ i += 1
370
+ else
371
+ value = token.object_for_literal()
372
+ if value.is_a?(TimeSpanWithZone)
373
+ time_span_with_zone = value
374
+ if time_span_with_zone.time_zone_offset
375
+ expecting_but_got(
376
+ "TIME SPAN",
377
+ "TIME (component of date/time)",
378
+ token.line,
379
+ token.position)
380
+ end
381
+
382
+ time_span = SdlTimeSpan.new(
383
+ time_span_with_zone.day,
384
+ time_span_with_zone.hour,
385
+ time_span_with_zone.min,
386
+ time_span_with_zone.sec)
387
+ tag.set_attribute(name_or_namespace, time_span)
388
+ else
389
+ tag.set_attribute(name_or_namespace, value);
390
+ end
391
+ end
392
+ else
393
+ expecting_but_got(
394
+ "\":\" or \"=\"", token.type, token.line, token.position)
395
+ end
396
+
397
+ i += 1
398
+ end
399
+ end
400
+
401
+ # Combines a simple Date with a TimeSpanWithZone to create a DateTime
402
+ #
403
+ def combine(date, time_span_with_zone)
404
+ time_zone_offset = time_span_with_zone.time_zone_offset
405
+ time_zone_offset = TimeSpanWithZone.default_time_zone_offset if time_zone_offset.nil?
406
+
407
+ return DateTime.new(
408
+ date.year,
409
+ date.month,
410
+ date.day,
411
+ time_span_with_zone.hour,
412
+ time_span_with_zone.min,
413
+ time_span_with_zone.sec,
414
+ time_zone_offset)
415
+ end
416
+
417
+ # An intermediate object used to store a timeSpan or the time
418
+ # component of a date/time instance. The types are disambiguated at a later stage.
419
+ #
420
+ # +seconds+ can have a fraction
421
+ # +time_zone_offset+ is a fraction of a day (equal to nil if not specified)
422
+ class TimeSpanWithZone
423
+
424
+ private
425
+
426
+ SECONDS_IN_DAY = 24 * 60 * 60
427
+
428
+ public
429
+
430
+ def initialize(day, hour, minute, second, time_zone_offset)
431
+ @day = day
432
+ @hour = hour
433
+ @min = minute
434
+ @sec = second
435
+ @time_zone_offset = time_zone_offset
436
+ end
437
+
438
+ attr_reader :day, :hour, :min, :sec, :time_zone_offset
439
+
440
+ # Returns the UTC offset as a fraction of a day on the current machine
441
+ def TimeSpanWithZone.default_time_zone_offset
442
+ return Rational(Time.now.utc_offset, SECONDS_IN_DAY)
443
+ end
444
+ end
445
+
446
+ private
447
+ ############################################################################
448
+ ## Parsers for types
449
+ ############################################################################
450
+
451
+ def Parser.parse_string(literal)
452
+ unless literal =~ /(^`.*`$)|(^\".*\"$)/m
453
+ raise ArgumentError,
454
+ "Malformed string <#{literal}>." +
455
+ " Strings must start and end with \" or `"
456
+ end
457
+
458
+ return literal[1..-2]
459
+ end
460
+
461
+ def Parser.parse_character(literal)
462
+ unless literal =~ /(^'.*'$)/
463
+ raise ArgumentError,
464
+ "Malformed character <#{literal}>." +
465
+ " Character must start and end with single quotes"
466
+ end
467
+
468
+ return literal[1]
469
+ end
470
+
471
+ def Parser.parse_number(literal)
472
+ # we use the fact that Kernel.Integer() and Kernel.Float() raise ArgumentErrors
473
+ if literal =~ /(.*)(L)$/i
474
+ return Integer($1)
475
+ elsif literal =~ /([^BDF]*)(BD)$/i
476
+ return (defined? Flt::DecNum) ? Flt::DecNum($1) : Float($1)
477
+ elsif literal =~ /([^BDF]*)(F|D)$/i
478
+ return Float($1)
479
+ elsif literal.count(".e") == 0
480
+ return Integer(literal)
481
+ else
482
+ return Float(literal)
483
+ end
484
+ end
485
+
486
+ # Parses the given literal into a returned array
487
+ # [days, hours, minutes, seconds, time_zone_offset].
488
+ # 'days', 'hours' and 'minutes' are integers.
489
+ # 'seconds' and 'time_zone_offset' are rational numbers.
490
+ # 'days' and 'seconds' are equal to 0 if they're not specified in ((|literal|)).
491
+ # 'time_zone_offset' is equal to nil if not specified.
492
+ #
493
+ # ((|allowDays|)) indicates whether the specification of days is allowed
494
+ # in ((|literal|))
495
+ # ((|allowTimeZone|)) indicates whether the specification of the timeZone is
496
+ # allowed in ((|literal|))
497
+ #
498
+ # All components are returned disregarding the values of ((|allowDays|)) and
499
+ # ((|allowTimeZone|)).
500
+ #
501
+ # Raises an ArgumentError if ((|literal|)) has a bad format.
502
+ def Parser.parse_time_span_and_time_zone(literal, allowDays, allowTimeZone)
503
+ overall_sign = (literal =~ /^-/)? -1 : +1
504
+
505
+ if literal =~ /^(([+\-]?\d+)d:)/
506
+ if allowDays
507
+ days = Integer($2)
508
+ days_specified = true
509
+ time_part = literal[($1.length)..-1]
510
+ else
511
+ # detected a day specification in a pure time literal
512
+ raise ArgumentError, "unexpected day specification in #{literal}"
513
+ end
514
+ else
515
+ days = 0;
516
+ days_specified = false
517
+ time_part = literal
518
+ end
519
+
520
+ # We have to parse the string ourselves because AFAIK :
521
+ # - strptime() can't parse milliseconds
522
+ # - strptime() can't parse the time zone custom offset (CET+02:30)
523
+ # - strptime() accepts trailing chars
524
+ # (e.g. "12:24-xyz@" ==> "xyz@" is obviously wrong but strptime()
525
+ # won't mind)
526
+ if time_part =~ /^([+-]?\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?(?:(?:-([a-zA-Z]+))?(?:([\+\-]\d+)(?::(\d+))?)?)?$/i
527
+ hours = $1.to_i
528
+ minutes = $2.to_i
529
+ # seconds and milliseconds are implemented as one rational number
530
+ # unless there are no milliseconds
531
+ millisecond_part = ($4)? $4.ljust(3, "0") : nil
532
+ if millisecond_part
533
+ seconds = Rational(($3 + millisecond_part).to_i, 10 ** millisecond_part.length)
534
+ else
535
+ seconds = ($3)? Integer($3) : 0
536
+ end
537
+
538
+ if ($5 or $6) and not allowTimeZone
539
+ raise ArgumentError, "unexpected time zone specification in #{literal}"
540
+ end
541
+
542
+ time_zone_code = $5 # might be nil
543
+
544
+ if $6
545
+ zone_custom_minute_offset = $6.to_i * 60
546
+ if $7
547
+ if zone_custom_minute_offset > 0
548
+ zone_custom_minute_offset = zone_custom_minute_offset + $7.to_i
549
+ else
550
+ zone_custom_minute_offset = zone_custom_minute_offset - $7.to_i
551
+ end
552
+ end
553
+ end
554
+
555
+ time_zone_offset = get_time_zone_offset(time_zone_code, zone_custom_minute_offset)
556
+
557
+ if not allowDays and $1 =~ /^[+-]/
558
+ # unexpected timeSpan syntax
559
+ raise ArgumentError, "unexpected sign on hours : #{literal}"
560
+ end
561
+
562
+ # take the sign into account
563
+ hours *= overall_sign if days_specified # otherwise the sign is already applied to the hours
564
+ minutes *= overall_sign
565
+ seconds *= overall_sign
566
+
567
+ return [ days, hours, minutes, seconds, time_zone_offset ]
568
+
569
+ else
570
+ raise ArgumentError, "bad time component : #{literal}"
571
+ end
572
+ end
573
+
574
+ # Parses the given literal (String) into a returned DateTime object.
575
+ #
576
+ # Raises an ArgumentError if ((|literal|)) has a bad format.
577
+ def Parser.parse_date_time(literal)
578
+ raise ArgumentError("date literal is nil") if literal.nil?
579
+
580
+ begin
581
+ parts = literal.split(" ")
582
+ if parts.length == 1
583
+ return parse_date(literal)
584
+ else
585
+ date = parse_date(parts[0]);
586
+ time_part = parts[1]
587
+
588
+ days, hours, minutes, seconds, time_zone_offset =
589
+ parse_time_span_and_time_zone(time_part, false, true)
590
+
591
+ return DateTime.civil(
592
+ date.year,
593
+ date.month,
594
+ date.day,
595
+ hours,
596
+ minutes,
597
+ seconds,
598
+ time_zone_offset)
599
+ end
600
+
601
+ rescue ArgumentError
602
+ raise ArgumentError, "Bad date/time #{literal} : #{$!.message}"
603
+ end
604
+ end
605
+
606
+ ##
607
+ # Returns the time zone offset (Rational) corresponding to the provided parameters as a fraction
608
+ # of a day. This method adds the two offsets if they are both provided.
609
+ #
610
+ # +time_zone_code+: can be nil
611
+ # +custom_minute_offset+: can be nil
612
+ #
613
+ def Parser.get_time_zone_offset(time_zone_code, custom_minute_offset)
614
+ return nil unless time_zone_code or custom_minute_offset
615
+
616
+ time_zone_offset = custom_minute_offset ? Rational(custom_minute_offset, 60 * 24) : 0
617
+
618
+ return time_zone_offset unless time_zone_code
619
+
620
+ # we have to provide some bogus year/month/day in order to parse our time zone code
621
+ d = DateTime.strptime("1999/01/01 #{time_zone_code}", "%Y/%m/%d %Z")
622
+ # the offset is a fraction of a day
623
+ return d.offset() + time_zone_offset
624
+ end
625
+
626
+ # Parses the +literal+ into a returned Date object.
627
+ #
628
+ # Raises an ArgumentError if +literal+ has a bad format.
629
+
630
+ def Parser.parse_date(literal)
631
+ # here, we're being stricter than strptime() alone as we forbid trailing chars
632
+ if literal =~ /^(\d+)\/(\d+)\/(\d+)$/
633
+ begin
634
+ return Date.strptime(literal, "%Y/%m/%d")
635
+ rescue ArgumentError
636
+ raise ArgumentError, "Malformed Date <#{literal}> : #{$!.message}"
637
+ end
638
+ end
639
+
640
+ raise ArgumentError, "Malformed Date <#{literal}>"
641
+ end
642
+
643
+ # Returns a String that contains the binary content corresponding to ((|literal|)).
644
+ #
645
+ # ((|literal|)) : a base-64 encoded literal (e.g.
646
+ # "[V2hvIHdhbnRzIHRvIGxpdmUgZm9yZXZlcj8=]")
647
+ def Parser.parse_binary(literal)
648
+ clean_literal = literal[1..-2] # remove square brackets
649
+ return SdlBinary.decode64(clean_literal)
650
+ end
651
+
652
+ # Parses +literal+ (String) into the corresponding SDLTimeSpan, which is then
653
+ # returned.
654
+ #
655
+ # Raises an ArgumentError if the literal is not a correct timeSpan literal.
656
+ def Parser.parse_time_span(literal)
657
+ days, hours, minutes, seconds, time_zone_offset =
658
+ parse_time_span_and_time_zone(literal, true, false)
659
+
660
+ milliseconds = ((seconds - seconds.to_i) * 1000).to_i
661
+ seconds = seconds.to_i
662
+
663
+ return SDLTimeSpan.new(days, hours, minutes, seconds, milliseconds)
664
+
665
+ raise ArgumentError,
666
+ "Malformed time span <#{literal}>. Time spans must use the format " +
667
+ "(d:)hh:mm:ss(.xxx) Note: if the day component is " +
668
+ "included it must be suffixed with lower case \"d\""
669
+ end
670
+
671
+ # Close the reader and throw a SdlParseError using the format
672
+ # Was expecting X but got Y.
673
+ #
674
+ def expecting_but_got(expecting, got, line, position)
675
+ @tokenizer.expecting_but_got(expecting, got, line, position)
676
+ end
677
+ end
678
+ end