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.
@@ -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.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,10 @@
1
+ module ISO8601
2
+ VERSION = "0.1.1"
3
+ end
4
+
5
+ require "time"
6
+
7
+ require "iso8601/errors"
8
+ require "iso8601/atoms"
9
+ require "iso8601/dateTime"
10
+ require "iso8601/duration"
@@ -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