iso8601 0.1.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/.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
|