sdl4r 0.9.1

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.
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