iso8601 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +504 -0
- data/README.md +28 -0
- data/Rakefile +1 -0
- data/iso8601.gemspec +26 -0
- data/lib/iso8601.rb +10 -0
- data/lib/iso8601/atoms.rb +114 -0
- data/lib/iso8601/dateTime.rb +104 -0
- data/lib/iso8601/duration.rb +140 -0
- data/lib/iso8601/errors.rb +24 -0
- data/test/iso8601/test_atoms.rb +100 -0
- data/test/iso8601/test_dateTime.rb +72 -0
- data/test/iso8601/test_duration.rb +91 -0
- data/test/test_iso8601.rb +4 -0
- metadata +83 -0
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ISO8601
|
2
|
+
|
3
|
+
ISO8601 is a simple implementation of the ISO 8601 (Data elements and
|
4
|
+
interchange formats — Information interchange — Representation of dates and
|
5
|
+
times) standard.
|
6
|
+
|
7
|
+
## Comments
|
8
|
+
|
9
|
+
Because Durations and DateTime has substract method, Durations has sign to represent a negative value:
|
10
|
+
|
11
|
+
* `(ISO8601::Duration.new("PT10S") - ISO8601::Duration.new("PT12S")).to_s #=> "-PT2S"`
|
12
|
+
* `(ISO8601::Duration.new("-PT10S") + ISO8601::Duration.new("PT12S")).to_s #=> "PT2S"`
|
13
|
+
|
14
|
+
|
15
|
+
## TODO
|
16
|
+
|
17
|
+
* Decimal fraction in dateTime and duration patterns
|
18
|
+
* Recurring time intervals
|
19
|
+
* Ordinal date pattern (YYYY-DDD)
|
20
|
+
* Week date pattern (YYYY-Www-D)
|
21
|
+
|
22
|
+
## Contributors
|
23
|
+
|
24
|
+
* [Nick Lynch](https://github.com/njlynch)
|
25
|
+
* [Pelle Braendgaard](https://github.com/pelle)
|
26
|
+
|
27
|
+
## Credits
|
28
|
+
Arnau Siches under [LGPL](http://www.gnu.org/licenses/lgpl.html) license. LICENSE file for details.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/iso8601.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "iso8601"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "iso8601"
|
7
|
+
s.version = ISO8601::VERSION
|
8
|
+
s.authors = ["Arnau Siches"]
|
9
|
+
s.email = ["arnau.siches@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/arnau/ISO8601"
|
11
|
+
s.summary = %q{Ruby parser to work with ISO8601 dateTimes and durations - http://en.wikipedia.org/wiki/ISO_8601}
|
12
|
+
s.description = %q{ISO8601 is a simple implementation of the ISO 8601 (Data elements and
|
13
|
+
interchange formats - Information interchange - Representation of dates and
|
14
|
+
times) standard.}
|
15
|
+
|
16
|
+
s.rubyforge_project = "iso8601"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
# specify any dependencies here; for example:
|
24
|
+
# s.add_development_dependency "rspec"
|
25
|
+
# s.add_runtime_dependency "rest-client"
|
26
|
+
end
|
data/lib/iso8601.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module ISO8601
|
2
|
+
|
3
|
+
# Represents a generic atom in a +ISO8601::Duration+.
|
4
|
+
class Atom
|
5
|
+
def initialize(atom, base=nil)
|
6
|
+
is_number?(atom, "First argument for #{self.inspect} must be an Integer or a Float.")
|
7
|
+
@atom = atom
|
8
|
+
@base = base
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_i
|
12
|
+
@atom
|
13
|
+
end
|
14
|
+
def to_seconds
|
15
|
+
@atom * self.factor
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def is_number?(arg, error_message=nil)
|
20
|
+
raise TypeError, error_message unless (arg.is_a? Integer or arg.is_a? Float)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# A “calendar year” is the cyclic time interval in a calendar which is
|
25
|
+
# required for one revolution of the Earth around the Sun and approximated to
|
26
|
+
# an integral number of “calendar days”.
|
27
|
+
|
28
|
+
#A “duration year” is the duration of 365 or 366 “calendar days” depending on
|
29
|
+
# the start and/or the end of the corresponding time interval within the
|
30
|
+
# specific “calendar year”.
|
31
|
+
class Years < ISO8601::Atom
|
32
|
+
|
33
|
+
# The “duration year” average is calculated through time intervals of 400
|
34
|
+
# “duration years”. Each cycle of 400 “duration years” has 303 “common
|
35
|
+
# years” of 365 “calendar days” and 97 “leap years” of 366 “calendar days”.
|
36
|
+
def factor
|
37
|
+
if @base.nil?
|
38
|
+
((365 * 303 + 366 * 97) / 400) * 86400
|
39
|
+
elsif @atom == 0
|
40
|
+
0
|
41
|
+
else
|
42
|
+
year = (@base.year + @atom).to_i
|
43
|
+
(Time.utc(year) - Time.utc(@base.year)) / @atom
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# A “calendar month” is the time interval resulting from the division of a
|
49
|
+
# “calendar year” in 12 time intervals.
|
50
|
+
|
51
|
+
# A “duration month” is the duration of 28, 29, 30 or 31 “calendar days”
|
52
|
+
# depending on the start and/or the end of the corresponding time interval
|
53
|
+
# within the specific “calendar month”.
|
54
|
+
class Months < ISO8601::Atom
|
55
|
+
|
56
|
+
# The “duration month” average is calculated through time intervals of 400
|
57
|
+
# “duration years”. Each cycle of 400 “duration years” has 303 “common
|
58
|
+
# years” of 365 “calendar days” and 97 “leap years” of 366 “calendar days”.
|
59
|
+
def factor
|
60
|
+
if @base.nil?
|
61
|
+
(((365 * 303 + 366 * 97) / 400) * 86400) / 12
|
62
|
+
elsif @atom == 0
|
63
|
+
0
|
64
|
+
else
|
65
|
+
month = (@base.month + @atom <= 12) ? (@base.month + @atom) : ((@base.month + @atom) % 12)
|
66
|
+
year = @base.year + ((@base.month + @atom) / 12).to_i
|
67
|
+
(Time.utc(year, month) - Time.utc(@base.year, @base.month)) / @atom
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Weeks < ISO8601::Atom
|
73
|
+
|
74
|
+
# A week is equal to 604800 seconds.
|
75
|
+
def factor
|
76
|
+
604800
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# A “calendar day” is the time interval which starts at a certain time of day
|
81
|
+
# at a certain “calendar day” and ends at the same time of day at the next
|
82
|
+
# “calendar day”.
|
83
|
+
class Days < ISO8601::Atom
|
84
|
+
|
85
|
+
# A day is equal to 86400 seconds.
|
86
|
+
def factor
|
87
|
+
86400
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Hours < ISO8601::Atom
|
92
|
+
|
93
|
+
# An hour is equal to 3600 seconds.
|
94
|
+
def factor
|
95
|
+
3600
|
96
|
+
end
|
97
|
+
end
|
98
|
+
class Minutes < ISO8601::Atom
|
99
|
+
|
100
|
+
# A minute is equal to 60 seconds.
|
101
|
+
def factor
|
102
|
+
60
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# The second is the base unit of measurement of time in the International
|
107
|
+
# System of Units (SI) as defined by the International Committee of Weights
|
108
|
+
# and Measures (CIPM, i.e. Comité International des Poids et Mesures)
|
109
|
+
class Seconds < ISO8601::Atom
|
110
|
+
def factor
|
111
|
+
1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ISO8601
|
2
|
+
class DateTime
|
3
|
+
attr_reader :date_time, :century, :year, :month, :day, :hour, :minute, :second, :timezone
|
4
|
+
def initialize(date_time)
|
5
|
+
@dt = /^(?:
|
6
|
+
(\d{2})(\d{2})?
|
7
|
+
(?:
|
8
|
+
(-)?(\d{2})
|
9
|
+
)?
|
10
|
+
(?:
|
11
|
+
(\3)?(\d{2})
|
12
|
+
)?
|
13
|
+
)?
|
14
|
+
(?:
|
15
|
+
T(\d{2})
|
16
|
+
(?:
|
17
|
+
(:)?(\d{2})
|
18
|
+
)?
|
19
|
+
(?:
|
20
|
+
(\8)?(\d{2})
|
21
|
+
)?
|
22
|
+
(
|
23
|
+
Z|([+-])
|
24
|
+
(\d{2})
|
25
|
+
(?:
|
26
|
+
(\8)?
|
27
|
+
(\d{2})
|
28
|
+
)?
|
29
|
+
)?
|
30
|
+
)?
|
31
|
+
$/x.match(date_time) or raise ISO8601::Errors::UnknownPattern.new(date_time)
|
32
|
+
|
33
|
+
@date_time = date_time
|
34
|
+
@time = @dt[7]
|
35
|
+
@date_separator = @dt[3] == @dt[5] ? @dt[3] : nil
|
36
|
+
@time_separator =
|
37
|
+
if (!@dt[8].nil? and (!@dt[10].nil? and !@dt[11].nil?) and (!@dt[15].nil? and !@dt[16].nil?)) or
|
38
|
+
(!@dt[8].nil? and (!@dt[10].nil? and !@dt[11].nil?) and @dt[16].nil?) or
|
39
|
+
(!@dt[8].nil? and @dt[11].nil? and @dt[16].nil?)
|
40
|
+
@dt[8]
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
@century = @dt[1].to_i
|
45
|
+
@year = @dt[2].nil? ? nil : (@dt[1] + @dt[2]).to_i
|
46
|
+
@month = @dt[4].nil? ? nil : @dt[4].to_i
|
47
|
+
@day = @dt[6].nil? ? nil : @dt[6].to_i
|
48
|
+
@hour = @dt[7].nil? ? nil : @dt[7].to_i
|
49
|
+
@minute = @dt[9].nil? ? nil : @dt[9].to_i
|
50
|
+
@second = @dt[11].nil? ? nil : @dt[11].to_i
|
51
|
+
@timezone = {
|
52
|
+
:full => @dt[12].nil? ? (Time.now.gmt_offset / 3600) : (@dt[12] == "Z" ? 0 : @dt[12]),
|
53
|
+
:sign => @dt[13],
|
54
|
+
:hour => @dt[12].nil? ? (Time.now.gmt_offset / 3600) : (@dt[12] == "Z" ? 0 : @dt[14].to_i),
|
55
|
+
:minute => (@dt[12].nil? or @dt[12] == "Z") ? 0 : @dt[14].to_i
|
56
|
+
}
|
57
|
+
|
58
|
+
valid_pattern?
|
59
|
+
valid_range?
|
60
|
+
end
|
61
|
+
def to_time
|
62
|
+
raise RangeError if @year.nil?
|
63
|
+
if @month.nil?
|
64
|
+
Time.utc(@year)
|
65
|
+
elsif @day.nil?
|
66
|
+
date = [@year, @month, '01'].join('-')
|
67
|
+
Time.parse(date).getutc
|
68
|
+
else
|
69
|
+
Time.parse(@date_time).getutc
|
70
|
+
end
|
71
|
+
end
|
72
|
+
def +(d)
|
73
|
+
raise TypeError unless (d.is_a? Float or d.is_a? Integer)
|
74
|
+
Time.utc(@year, @month, @day, @hour, @minute, @second) + d
|
75
|
+
end
|
76
|
+
def -(d)
|
77
|
+
raise TypeError unless (d.is_a? Float or d.is_a? Integer or d.is_a? ISO8601::DateTime)
|
78
|
+
if (d.is_a? ISO8601::DateTime)
|
79
|
+
Time.utc(@year, @month, @day, @hour, @minute, @second) - Time.utc(d.year, d.month, d.day, d.hour, d.minute, d.second)
|
80
|
+
else
|
81
|
+
Time.utc(@year, @month, @day, @hour, @minute, @second) - d
|
82
|
+
end
|
83
|
+
end
|
84
|
+
private
|
85
|
+
def valid_pattern?
|
86
|
+
if (@date_separator.nil? and !@time_separator.nil?) or
|
87
|
+
(!@date_separator.nil? and !@time.nil? and @time_separator.nil? and !@minute.nil?) or
|
88
|
+
(@year.nil? and !@month.nil?)
|
89
|
+
raise ISO8601::Errors::UnknownPattern.new(@date_time)
|
90
|
+
elsif (@year.nil? and @month.nil?)
|
91
|
+
@year = (@century.to_s + "00").to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
def valid_range?
|
95
|
+
if !month.nil? and (month < 1 or month > 12)
|
96
|
+
raise RangeError
|
97
|
+
elsif !day.nil? and (Time.parse(@date_time).month != month)
|
98
|
+
raise RangeError
|
99
|
+
end
|
100
|
+
rescue ArgumentError => e
|
101
|
+
raise RangeError
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module ISO8601
|
2
|
+
|
3
|
+
# Represents a duration in ISO 8601 format
|
4
|
+
class Duration
|
5
|
+
attr_reader :base, :atoms
|
6
|
+
def initialize(duration, base = nil)
|
7
|
+
@duration = /^(\+|-)?P(((\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)|(\d+W))$/.match(duration)
|
8
|
+
@base = base #date base for duration calculations
|
9
|
+
valid_pattern?
|
10
|
+
valid_base?
|
11
|
+
@atoms = {
|
12
|
+
:years => @duration[4].nil? ? 0 : @duration[4].chop.to_f * sign,
|
13
|
+
:months => @duration[5].nil? ? 0 : @duration[5].chop.to_f * sign,
|
14
|
+
:weeks => @duration[11].nil? ? 0 : @duration[11].chop.to_f * sign,
|
15
|
+
:days => @duration[6].nil? ? 0 : @duration[6].chop.to_f * sign,
|
16
|
+
:hours => @duration[8].nil? ? 0 : @duration[8].chop.to_f * sign,
|
17
|
+
:minutes => @duration[9].nil? ? 0 : @duration[9].chop.to_f * sign,
|
18
|
+
:seconds => @duration[10].nil? ? 0 : @duration[10].chop.to_f * sign
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def base=(value)
|
23
|
+
@base = value
|
24
|
+
return @base
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the original string of the duration
|
28
|
+
def to_s
|
29
|
+
@duration[0]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the years of the duration
|
33
|
+
def years
|
34
|
+
ISO8601::Years.new(@atoms[:years], @base)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the months of the duration
|
38
|
+
def months
|
39
|
+
base = @base.nil? ? nil : @base + self.years.to_seconds # prevent computing duplicated time
|
40
|
+
ISO8601::Months.new(@atoms[:months], base)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the weeks of the duration
|
44
|
+
def weeks
|
45
|
+
ISO8601::Weeks.new(@atoms[:weeks], @base)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the days of the duration
|
49
|
+
def days
|
50
|
+
ISO8601::Days.new(@atoms[:days], @base)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the hours of the duration
|
54
|
+
def hours
|
55
|
+
ISO8601::Hours.new(@atoms[:hours], @base)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the minutes of the duration
|
59
|
+
def minutes
|
60
|
+
ISO8601::Minutes.new(@atoms[:minutes], @base)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the seconds of the duration
|
64
|
+
def seconds
|
65
|
+
ISO8601::Seconds.new(@atoms[:seconds], @base)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the duration in seconds
|
69
|
+
def to_seconds
|
70
|
+
years, months, weeks, days, hours, minutes, seconds = self.years.to_seconds, self.months.to_seconds, self.weeks.to_seconds, self.days.to_seconds, self.hours.to_seconds, self.minutes.to_seconds, self.seconds.to_seconds
|
71
|
+
return years + months + weeks + days + hours + minutes + seconds
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the absolute value of duration
|
75
|
+
def abs
|
76
|
+
return self.to_s.sub!(/^[-+]/, "")
|
77
|
+
end
|
78
|
+
|
79
|
+
def +(duration)
|
80
|
+
raise ISO8601::Errors::DurationBaseError.new(duration) if self.base != duration.base
|
81
|
+
d1 = self.to_seconds
|
82
|
+
d2 = duration.to_seconds
|
83
|
+
return self.seconds_to_iso(d1 + d2)
|
84
|
+
end
|
85
|
+
def -(duration)
|
86
|
+
raise ISO8601::Errors::DurationBaseError.new(duration) if self.base != duration.base
|
87
|
+
d1 = self.to_seconds
|
88
|
+
d2 = duration.to_seconds
|
89
|
+
return self.seconds_to_iso(d1 - d2)
|
90
|
+
# return d1 - d2
|
91
|
+
end
|
92
|
+
|
93
|
+
# Convenience method to turn instance method (which can take into
|
94
|
+
# account a base time or duration) into a simple class method.
|
95
|
+
def self.seconds_to_iso(duration)
|
96
|
+
return ISO8601::Duration.new('P0Y').seconds_to_iso(duration)
|
97
|
+
end
|
98
|
+
|
99
|
+
def seconds_to_iso(duration)
|
100
|
+
sign = "-" if (duration < 0)
|
101
|
+
duration = duration.abs
|
102
|
+
years, y_mod = (duration / self.years.factor).to_i, (duration % self.years.factor)
|
103
|
+
months, m_mod = (y_mod / self.months.factor).to_i, (y_mod % self.months.factor)
|
104
|
+
days, d_mod = (m_mod / self.days.factor).to_i, (m_mod % self.days.factor)
|
105
|
+
hours, h_mod = (d_mod / self.hours.factor).to_i, (d_mod % self.hours.factor)
|
106
|
+
minutes, mi_mod = (h_mod / self.minutes.factor).to_i, (h_mod % self.minutes.factor)
|
107
|
+
seconds = mi_mod.to_i
|
108
|
+
|
109
|
+
seconds = (seconds != 0 or (years == 0 and months == 0 and days == 0 and hours == 0 and minutes == 0)) ? "#{seconds}S" : ""
|
110
|
+
minutes = (minutes != 0) ? "#{minutes}M" : ""
|
111
|
+
hours = (hours != 0) ? "#{hours}H" : ""
|
112
|
+
days = (days != 0) ? "#{days}D" : ""
|
113
|
+
months = (months != 0) ? "#{months}M" : ""
|
114
|
+
years = (years != 0) ? "#{years}Y" : ""
|
115
|
+
|
116
|
+
date = %[#{sign}P#{years}#{months}#{days}]
|
117
|
+
time = (hours != "" or minutes != "" or seconds != "") ? %[T#{hours}#{minutes}#{seconds}] : ""
|
118
|
+
date_time = date + time
|
119
|
+
return ISO8601::Duration.new(date_time)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
def sign
|
124
|
+
(@duration[1].nil? or @duration[1] == "+") ? 1 : -1
|
125
|
+
end
|
126
|
+
def valid_base?
|
127
|
+
if !(@base.is_a? NilClass or @base.is_a? ISO8601::DateTime)
|
128
|
+
raise TypeError
|
129
|
+
end
|
130
|
+
end
|
131
|
+
def valid_pattern?
|
132
|
+
if @duration.nil? or
|
133
|
+
(@duration[4].nil? and @duration[5].nil? and @duration[6].nil? and @duration[7].nil? and @duration[11].nil?) or
|
134
|
+
(!@duration[7].nil? and @duration[8].nil? and @duration[9].nil? and @duration[10].nil? and @duration[11].nil?)
|
135
|
+
|
136
|
+
raise ISO8601::Errors::UnknownPattern.new(@duration)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module ISO8601
|
3
|
+
|
4
|
+
# Contains all ISO8601-specific errors.
|
5
|
+
module Errors
|
6
|
+
|
7
|
+
# Error that is raised when unknown pattern is parsed.
|
8
|
+
class UnknownPattern < ::StandardError
|
9
|
+
def initialize(pattern)
|
10
|
+
super("The pattern “#{pattern}” is not allowed in this implementation of ISO8601.")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class DurationError < ::StandardError
|
14
|
+
def initialize(duration)
|
15
|
+
super("Unexpected type of duration “#{duration}”.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class DurationBaseError < ISO8601::Errors::DurationError
|
19
|
+
def initialize(duration)
|
20
|
+
super("Wrong base for #{duration} duration.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|