timezone 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ require 'timezone/parser/rule'
2
+ require 'timezone/parser/data'
3
+ require 'timezone/parser/zone'
4
+ require 'timezone/parser/zone/data_generator'
5
+
6
+ module Timezone
7
+ module Parser
8
+ COMMENT_REGEXP = /^\s*#/
9
+ RULE_REGEXP = /^Rule/
10
+ LINK_REGEXP = /^Link/
11
+ ZONE_REGEXP = /^Zone/
12
+
13
+ def self.parse(file)
14
+ IO.readlines(file).map(&:strip).each do |line|
15
+ if line =~ COMMENT_REGEXP
16
+ next
17
+ elsif line =~ RULE_REGEXP
18
+ rule(line)
19
+ elsif line =~ LINK_REGEXP
20
+ # TODO [panthomakos] Need to add linking.
21
+ elsif line =~ ZONE_REGEXP || (line != '' && !line.nil?)
22
+ zone(line)
23
+ else
24
+ Timezone::Parser::Zone.generate(Timezone::Parser::Zone.last)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,103 @@
1
+ require 'json'
2
+
3
+ module Timezone
4
+ module Parser
5
+ def self.data(*args) ; Data.new(*args) ; end
6
+
7
+ def self.from_zone(previous, zone)
8
+ data(previous && previous.end_date, zone.end_date, zone)
9
+ end
10
+
11
+ def self.extension(previous, zone)
12
+ data(previous.end_date, nil, zone)
13
+ end
14
+
15
+ def self.from_rule(zone, rule)
16
+ data(rule.start_date, nil, zone, rule)
17
+ end
18
+
19
+ START_DATE = -377705116800000 # The very last date '9999-12-31T23:59:59Z'.
20
+ END_DATE = 253402300799000 # The very first date '-9999-01-01T00:00:00Z'.
21
+
22
+ class NilRule
23
+ def letter ; '-' ; end
24
+ def offset ; 0 ; end
25
+ def utime? ; false ; end
26
+ def dst? ; false ; end
27
+ end
28
+
29
+ # The resulting JSON data structure for a timezone file.
30
+ class Data
31
+ attr_accessor :start_date, :dst, :offset, :name
32
+
33
+ def initialize(start_date, end_date, zone, rule = NilRule.new)
34
+ @dst = rule.dst?
35
+ @offset = parse_offset(zone, rule)
36
+ @name = parse_name(zone.format, rule.letter)
37
+ @utime = rule.utime?
38
+ @start_date = parse_start_date(start_date)
39
+ self.end_date = end_date
40
+ end
41
+
42
+ def end_date=(date)
43
+ if date.nil? || @utime
44
+ @end_date = date
45
+ else
46
+ @end_date = date - (@offset * 1_000)
47
+ end
48
+ end
49
+
50
+ def end_date
51
+ @end_date || END_DATE
52
+ end
53
+
54
+ def has_end_date?
55
+ !!@end_date
56
+ end
57
+
58
+ def to_hash
59
+ {
60
+ '_from' => _from,
61
+ 'from' => @start_date,
62
+ '_to' => _to,
63
+ 'to' => end_date,
64
+ 'dst' => @dst,
65
+ 'offset' => @offset,
66
+ 'name' => @name
67
+ }
68
+ end
69
+
70
+ def to_json
71
+ to_hash.to_json
72
+ end
73
+
74
+ private
75
+
76
+ def parse_offset(zone, rule)
77
+ zone.offset + rule.offset
78
+ end
79
+
80
+ def parse_start_date(date)
81
+ date || START_DATE
82
+ end
83
+
84
+ # Fills in a zone entry format with a rule entry letter.
85
+ #
86
+ # Examples:
87
+ #
88
+ # format "EE%sT" w/ letter "S" results in EEST
89
+ # format "EE%sT" w/ letter "-" results in EET
90
+ def parse_name(format, letter)
91
+ format.sub(/%s/, letter == '-' ? '' : letter)
92
+ end
93
+
94
+ def _from ; ftime(@start_date) ; end
95
+ def _to ; ftime(end_date) ; end
96
+
97
+ # Converts a millisecond time into the proper JSON output string.
98
+ def ftime(time)
99
+ Time.at(time / 1_000).utc.strftime('%Y-%m-%dT%H:%M:%SZ')
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,10 @@
1
+ module Timezone
2
+ module Parser
3
+ def self.link(line) ; Link.new(line) ; end
4
+
5
+ class Link
6
+ def initialize(line)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,55 @@
1
+ require 'time'
2
+
3
+ module Timezone
4
+ module Parser
5
+ # Given a rule from a TZDATA file, generate the appropriate rule objects.
6
+ def self.rule(line) ; Rule.generate(line) ; end
7
+
8
+ # Get a list of all processed rules.
9
+ def self.rules ; Rule.rules ; end
10
+
11
+ # Select rules based on a name and end date.
12
+ def self.select_rules(name, end_date)
13
+ rules.fetch(name){ [] }
14
+ .select{ |rule| end_date.nil? || rule.start_date < end_date }
15
+ end
16
+
17
+ module Rule
18
+ # Format: Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
19
+ # Example: Rule EUAsia 1981 max - Mar lastSun 1:00u 1:00 S
20
+ RULE = /Rule\s+([\w-]+?)\s+(\d+?)\s+([^\s]+?)\s+([^\s]+?)\s+(\s*\w+?)\s+([\d\w\=\>\<]+?)\s+([\d:us]+?)\s+([\d:]+?)\s+([\w-]+)/
21
+
22
+ END_YEAR = 2050 # The actual value for the "to" field when set to "max".
23
+
24
+ # Rules are stored in a hash of arrays that are referenced by rule name.
25
+ @@rules = Hash.new{ |h, k| h[k] = [] }
26
+ def self.rules ; @@rules ; end
27
+
28
+ class << self
29
+ def generate(line)
30
+ name, from, to, *values = *line.match(RULE)[1..-1]
31
+
32
+ years(from, to).each do |year|
33
+ @@rules[name] << Entry.new(name, year, *values)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def years(from, to)
40
+ (from.to_i..parse_end_year(from, to))
41
+ end
42
+
43
+ def parse_end_year(from, to)
44
+ case to
45
+ when 'only' then from.to_i
46
+ when 'max' then END_YEAR
47
+ else to.to_i
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ require 'timezone/parser/rule/entry'
@@ -0,0 +1,74 @@
1
+ require 'timezone/parser/rule'
2
+ require 'timezone/parser/rule/on_rules'
3
+
4
+ module Timezone::Parser::Rule
5
+ class Entry
6
+ attr_accessor :name, :offset
7
+ attr_reader :letter, :start_date
8
+
9
+ UTIME = /^.*u$/
10
+ STIME = /^.*s$/
11
+ START_DATE = '%Y %b %d %H:%M %Z'
12
+
13
+ def initialize(name, year, type, month, day, time, save, letter)
14
+ @name, @offset, @letter = name, offset, letter
15
+
16
+ @month, @day = parse_month_day(day, month, year)
17
+ @utime = parse_utime(time)
18
+ @stime = parse_stime(time)
19
+ @time = parse_time(time)
20
+ @day = parse_day(@day)
21
+ @offset = parse_offset(save)
22
+ @start_date = parse_start_date(year, @month, @day, @time)
23
+ @dst = parse_dst(save)
24
+ end
25
+
26
+ def utime? ; @utime ; end
27
+ def stime? ; @stime ; end
28
+ def dst? ; @dst ; end
29
+
30
+ private
31
+
32
+ # Day should be zero padded.
33
+ def parse_day(day)
34
+ '%.2d' % day.to_i
35
+ end
36
+
37
+ # Time should be zero padded and not include 'u' or 's'.
38
+ def parse_time(time)
39
+ time = "0#{time}" if time.match(/^\d:\d\d/)
40
+ time = time.gsub(/u/, '') if utime?
41
+ time = time.gsub(/s/, '') if stime?
42
+
43
+ time
44
+ end
45
+
46
+ def parse_utime(time)
47
+ time =~ UTIME
48
+ end
49
+
50
+ def parse_stime(time)
51
+ time =~ STIME
52
+ end
53
+
54
+ # Offset is calculated in seconds.
55
+ def parse_offset(save)
56
+ offset = Time.parse(save == '0' ? '0:00' : save)
57
+ offset.hour*60*60 + offset.min*60 + offset.sec
58
+ end
59
+
60
+ # Check special rules that modify the month and day depending on the year.
61
+ def parse_month_day(day, month, year)
62
+ On.parse(day, month, year)
63
+ end
64
+
65
+ # The UTC time on which the rule beings to apply in milliseconds.
66
+ def parse_start_date(y, m, d, t)
67
+ Time.strptime([y, m, d, t, 'UTC'].join(' '), START_DATE).to_i * 1_000
68
+ end
69
+
70
+ def parse_dst(save)
71
+ save != '0'
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ require 'timezone/parser/rule'
2
+ require 'time'
3
+
4
+ module Timezone::Parser::Rule
5
+ def self.on(*args) ; On.new(*args) ; end
6
+
7
+ class On
8
+ @@rules = []
9
+
10
+ # Given a Rule `on` field, parse the appropriate day and month.
11
+ def self.parse(day, month, year)
12
+ @@rules.each do |rule|
13
+ if match = day.match(rule.expression)
14
+ return rule.block.call(match, day, month, year)
15
+ end
16
+ end
17
+
18
+ [month, day]
19
+ end
20
+
21
+ attr_reader :expression, :block
22
+
23
+ def initialize(name, expression, block)
24
+ @name = name
25
+ @expression = expression
26
+ @block = block
27
+ @@rules << self
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ require 'timezone/parser/rule/on'
2
+
3
+ # A simple DSL for definining rules to parse the "ON" field in TZData rules.
4
+ module Timezone::Parser::Rule
5
+ on 'lastDAY', /^last(\w+)$/, lambda{ |match, _, month, year|
6
+ 31.downto(1).each do |day|
7
+ begin
8
+ date = Time.strptime("#{year} #{month} #{day}", '%Y %b %d')
9
+
10
+ if date.strftime('%a') == match[1]
11
+ return [month, date.strftime('%d')]
12
+ end
13
+ rescue
14
+ next
15
+ end
16
+ end
17
+ }
18
+
19
+ on 'DAY>=NUM', /^(\w+)>=(\d+)$/, lambda{ |match, _, month, year|
20
+ start = Time.strptime("#{year} #{month} #{match[2]}", '%Y %b %d')
21
+
22
+ (1..8).to_a.each do |plus|
23
+ date = start + (plus * 24 * 60 * 60)
24
+
25
+ if date.strftime('%a') == match[1]
26
+ return [date.strftime('%b'), date.strftime('%d')]
27
+ end
28
+ end
29
+ }
30
+ end
@@ -0,0 +1,37 @@
1
+ require 'time'
2
+
3
+ module Timezone
4
+ module Parser
5
+ # Given a line from the TZDATA file, generate an Entry object.
6
+ def self.zone(line) ; Zone.parse(line) ; end
7
+
8
+ # Get a list of all processed entries.
9
+ def self.zones ; Zone.zones ; end
10
+
11
+ module Zone
12
+ # Each entry follows this format.
13
+ # GMT-OFFSET RULES FORMAT [UNTIL]
14
+ ENTRY = /(\d+?:\d+?:*\d*?)\s+(.+?)\s([^\s]+)\s*(.*?)$/
15
+
16
+ # The header entry also includes the Zone name.
17
+ # Zone ZONE-NAME GMT-OFFSET RULES FORMAT [UNTIL]
18
+ HEADER = /Zone\s+(.+?)\s+/
19
+
20
+ # Zones are stored in a hash of arrays that are referenced by name.
21
+ @@zones = Hash.new{ |h, k| h[k] = [] }
22
+ def self.zones ; @@zones ; end
23
+
24
+ # The name of the current zone is parsed from the header zone entry.
25
+ # It can then be accessed using `Timezone::Parser::Zone.last`.
26
+ class << self ; attr_accessor :last ; end
27
+
28
+ def self.parse(line)
29
+ self.last = $~[1] if line.match(HEADER)
30
+
31
+ @@zones[last] << Entry.new(last, *line.match(ENTRY)[1..-1])
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ require 'timezone/parser/zone/entry'
@@ -0,0 +1,77 @@
1
+ require 'timezone/parser/zone'
2
+ require 'timezone/parser/data'
3
+ require 'time'
4
+
5
+ module Timezone::Parser::Zone
6
+ def self.generate(zones) ; DataGenerator.generate(zones) ; end
7
+
8
+ def self.data ; DataGenerator.data ; end
9
+
10
+ # TODO [panthomakos] This needs refactoring.
11
+ module DataGenerator
12
+ @@data = Hash.new{ |h,k| h[k] = [] }
13
+ def self.data ; @@data ; end
14
+
15
+ class << self
16
+ def generate(zone)
17
+ zones = Timezone::Parser.zones[zone].to_a
18
+
19
+ return if zones.empty?
20
+
21
+ @@data[zone] = zones
22
+ .each_cons(2)
23
+ .inject(update(zones.first)) do |set, (previous, zone)|
24
+ update(zone, set, previous && previous.end_date)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def update(zone, set = [], limit = nil)
31
+ additions = [first_addition(zone, set)]
32
+
33
+ zone.rules.each do |rule|
34
+ data = additions.last
35
+
36
+ if rule_applies?(rule, data, limit)
37
+ Timezone::Parser.from_rule(zone, rule).tap do |insert|
38
+ data.end_date = insert.start_date
39
+ # We do this because the start date is always based on the
40
+ # previous entry end date calculation.
41
+ insert.start_date = data.end_date
42
+
43
+ additions << insert
44
+ end
45
+ end
46
+ end
47
+
48
+ set + additions
49
+ end
50
+
51
+ # The rule has to fall within the time range (start_date..limit).
52
+ def rule_applies?(rule, data, limit)
53
+ rule.start_date > data.start_date &&
54
+ (!limit || rule.start_date > limit)
55
+ end
56
+
57
+ def first_addition(zone, set)
58
+ previous = set.last
59
+
60
+ if zone.rules.empty?
61
+ # If there are no rules, generate a new entry for this time period.
62
+ Timezone::Parser.from_zone(previous, zone)
63
+ else
64
+ if previous && previous.has_end_date?
65
+ # If the last entry had a hard cutoff end date, create a new
66
+ # addition that picks up from where the last entry left off.
67
+ Timezone::Parser.extension(previous, zone)
68
+ else
69
+ # If the last entry did not have a hard cutoff end date, pop it off
70
+ # the stack for use in these calculations.
71
+ set.pop
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end