iso8601 0.4.1 → 0.5.0

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7f9e6e9d7f640ea9bb6d3393080c7fe50f4a1b68
4
+ data.tar.gz: 45717b674acf7e8eb1a5193f398d52320d4fce89
5
+ SHA512:
6
+ metadata.gz: 40249de2e3e817cb21e7c441ab3ffd1e18a852ebaf76241e913f9275924dc710dd0d9acad440778c5241ff0d6a8be7f430bab6ae9ebc66aaf03ff5373c16f9d2
7
+ data.tar.gz: 3733a36fb5764007920b519bed2cc938d76382416ab6592140b74dbd32ca014c8804d8ce973efe7aea9c55ca28db3cdadefcf051469b4659d8d3d670c0aa3fac
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
- - 1.9.2
5
3
  - 1.9.3
6
4
  - 2.0.0
5
+ - 2.1.2
6
+ - rbx-2
7
7
  script: bundle exec rspec spec
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Arnau Siches
1
+ Copyright (c) 2012-2014 Arnau Siches
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,46 +1,107 @@
1
1
  # ISO8601
2
2
 
3
- ISO8601 is a simple implementation of the ISO 8601 (Data elements and
4
- interchange formats — Information interchange — Representation of dates and
3
+ ISO8601 is a simple implementation of the ISO 8601 (Data elements and
4
+ interchange formats — Information interchange — Representation of dates and
5
5
  times) standard.
6
6
 
7
7
  ## Build status
8
8
 
9
9
  [![Build Status](https://secure.travis-ci.org/arnau/ISO8601.png?branch=master)](http://travis-ci.org/arnau/ISO8601/)
10
10
 
11
+ ## Supported versions
12
+
13
+ * MRI 1.9.3
14
+ * MRI 2.0
15
+ * MRI 2.1
16
+ * RBX 2
17
+
11
18
 
12
19
  ## Comments
13
20
 
14
21
  ### Duration sign
15
22
 
16
- Because Durations and DateTime has a substraction method, Durations has sign to be able to represent a negative value:
23
+ Because Durations and DateTime has a substraction method, Durations has
24
+ sign to be able to represent a negative value:
17
25
 
18
- (ISO8601::Duration.new('PT10S') - ISO8601::Duration.new('PT12S')).to_s #=> '-PT2S'
26
+ (ISO8601::Duration.new('PT10S') - ISO8601::Duration.new('PT12S')).to_s #=> '-PT2S'
19
27
  (ISO8601::Duration.new('-PT10S') + ISO8601::Duration.new('PT12S')).to_s #=> 'PT2S'
20
28
 
21
- ### Separators
22
29
 
23
- Although, the spec allows three separator types: period (.), comma (,), and raised period (·) by now I keep just the period option.
30
+ ## Differences with core Date, Time and DateTime
31
+
32
+ Core `Date.parse` and `DateTime.parse` doesn't allow reduced precision. For
33
+ example:
34
+
35
+ DateTime.parse('2014-05') # => ArgumentError: invalid date
36
+
37
+ But the standard covers this situation assuming any missing piece as its lower
38
+ value:
39
+
40
+ ISO8601::DateTime.new('2014-05').to_s # => "2014-05-01T00:00:00+00:00"
41
+ ISO8601::DateTime.new('2014').to_s # => "2014-01-01T00:00:00+00:00"
42
+
43
+ The same assumption happens in core classes with `.new`:
44
+
45
+ DateTime.new(2014,5) # => #<DateTime: 2014-05-01T00:00:00+00:00 ((2456779j,0s,0n),+0s,2299161j)>
46
+ DateTime.new(2014) # => #<DateTime: 2014-01-01T00:00:00+00:00 ((2456659j,0s,0n),+0s,2299161j)>
47
+
48
+
49
+ The value of second in core classes are handled by two methods: `#second` and
50
+ `#second_fraction`:
51
+
52
+ dt = DateTime.parse('2014-05-06T10:11:12.5')
53
+ dt.second # => 12
54
+ dt.second_fraction # => (1/2)
55
+
56
+ This gem approaches second fraction using floats:
24
57
 
25
- ### Century treatment
58
+ dt = ISO8601::DateTime.new('2014-05-06T10:11:12.5')
59
+ dt.second # => 12.5
26
60
 
27
- The specification says that you can express a reduced precision year
28
- just giving the century (i.e. '20' to refer the inclusive range 2000-2999).
61
+ Unmatching precison is handled strongly. Notice the time fragment is lost in
62
+ `DateTime.parse` without warning only if the loose precision is in the time
63
+ component.
29
64
 
30
- This implementation expands the century to the first value for its range
31
- so:
65
+ ISO8601::DateTime.new('2014-05-06T101112') # => ISO8601::Errors::UnknownPattern
66
+ DateTime.parse('2014-05-06T101112') # => #<DateTime: 2014-05-06T00:00:00+00:00 ((2456784j,0s,0n),+0s,2299161j)>
32
67
 
33
- ISO8601::DateTime.new('20').century # => 20
34
- ISO8601::DateTime.new('20').year # => 2000
68
+ ISO8601::DateTime.new('20140506T10:11:12') # => ISO8601::Errors::UnknownPattern
69
+ DateTime.parse('20140506T10:11:12') # => #<DateTime: 2014-05-06T10:11:12+00:00 ((2456784j,0s,0n),+0s,2299161j)>
70
+
71
+
72
+ `DateTime#to_a` allow decomposing to an array of atoms:
73
+
74
+ atoms = ISO8601::DateTime.new('2014-05-31T10:11:12Z').to_a # => [2014, 5, 31, 10, 11, 12, '+00:00']
75
+ dt = DateTime.new(*atoms)
76
+
77
+ Ordinal dates keep the sign. `2014-001` is not the same as `-2014-001`.
78
+
79
+ Week dates raise an error when two digit days provied instead of return monday:
80
+
81
+ ISO8601::DateTime.new('2014-W15-02') # => ISO8601::Errors::UnknownPattern
82
+ DateTime.new('2014-W15-02') # => #<Date: 2014-04-07 ((2456755j,0s,0n),+0s,2299161j)>
83
+
84
+
85
+ ## Changes since 0.5
86
+
87
+ * Drop support for Ruby 1.8.7
88
+ * Add support for Rubinius 2
89
+ * `ISO8601::DateTime#century` no longer exists. Truncated representations were
90
+ removed in ISO 8601:2004.
91
+ * `ISO8601::DateTime#zone` delegates to core `DateTime#zone`.
92
+ * `ISO8601::DateTime#timezone` no longer exists. Now it delegates to
93
+ `DateTime#zone`.
94
+ * A date can have sign: `-1000-01-01`, `+2014-05-06T10:11:12Z`.
95
+ * A date time can be converted to an array of atoms with `#to_a`.
96
+ * Ordinal dates supported.
97
+ * A date component is represented by `ISO8601::Date`.
98
+ * Week date pattern (YYYY-Wdww, YYYY-Www-D).
35
99
 
36
100
 
37
101
  ## TODO
38
102
 
39
- * Decimal fraction in dateTime patterns
40
103
  * Recurring time intervals
41
- * Ordinal date pattern (YYYY-DDD)
42
- * Week date pattern (YYYY-Www-D)
43
- * Treat the `201005` as `2000-10-05` instead of `2010-05`
104
+
44
105
 
45
106
  ## Contributors
46
107
 
@@ -53,4 +114,3 @@ so:
53
114
  ## License
54
115
 
55
116
  Arnau Siches under the [MIT License](https://github.com/arnau/ISO8601/blob/master/LICENSE)
56
-
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  $:.push File.expand_path('../lib', __FILE__)
4
- require 'iso8601'
4
+ require 'iso8601/version'
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = 'iso8601'
@@ -25,6 +25,6 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.has_rdoc = 'yard'
27
27
 
28
- s.add_development_dependency 'rspec', '~> 2.0'
29
- s.add_development_dependency 'ZenTest', '~> 4.8'
28
+ s.add_development_dependency 'rspec', '~> 2.14'
29
+ s.add_development_dependency 'rerun'
30
30
  end
@@ -1,12 +1,12 @@
1
1
  # encoding: utf-8
2
2
 
3
- module ISO8601
4
- VERSION = '0.4.1'
5
- end
6
-
7
3
  require 'time'
4
+ require 'forwardable'
8
5
 
6
+ require 'iso8601/version'
9
7
  require 'iso8601/errors'
10
8
  require 'iso8601/atoms'
9
+ require 'iso8601/date'
10
+ require 'iso8601/time'
11
11
  require 'iso8601/dateTime'
12
12
  require 'iso8601/duration'
@@ -54,10 +54,10 @@ module ISO8601
54
54
  ((365 * 303 + 366 * 97) / 400) * 86400
55
55
  elsif @atom == 0
56
56
  year = (@base.year).to_i
57
- (Time.utc(year) - Time.utc(@base.year))
57
+ (::Time.utc(year) - ::Time.utc(@base.year))
58
58
  else
59
59
  year = (@base.year + @atom).to_i
60
- (Time.utc(year) - Time.utc(@base.year)) / @atom
60
+ (::Time.utc(year) - ::Time.utc(@base.year)) / @atom
61
61
  end
62
62
  end
63
63
  end
@@ -83,11 +83,11 @@ module ISO8601
83
83
  elsif @atom == 0
84
84
  month = (@base.month <= 12) ? (@base.month) : ((@base.month) % 12)
85
85
  year = @base.year + ((@base.month) / 12).to_i
86
- (Time.utc(year, month) - Time.utc(@base.year, @base.month))
86
+ (::Time.utc(year, month) - ::Time.utc(@base.year, @base.month))
87
87
  else
88
88
  month = (@base.month + @atom <= 12) ? (@base.month + @atom) : ((@base.month + @atom) % 12)
89
89
  year = @base.year + ((@base.month + @atom) / 12).to_i
90
- (Time.utc(year, month) - Time.utc(@base.year, @base.month)) / @atom
90
+ (::Time.utc(year, month) - ::Time.utc(@base.year, @base.month)) / @atom
91
91
  end
92
92
  end
93
93
  end
@@ -0,0 +1,125 @@
1
+ module ISO8601
2
+ ##
3
+ # A Date representation
4
+ #
5
+ # @example
6
+ # d = Date.new('2014-05-28')
7
+ # d.year # => 2014
8
+ # d.month # => 5
9
+ #
10
+ # @example Week dates
11
+ # d = Date.new('2014-W15-2')
12
+ # d.day # => 27
13
+ # d.wday # => 2
14
+ # d.week # => 15
15
+ class Date
16
+ extend Forwardable
17
+
18
+ def_delegators(:@date,
19
+ :to_s, :to_time, :to_date, :to_datetime,
20
+ :year, :month, :day, :wday)
21
+ ##
22
+ # The original atoms
23
+ attr_reader :atoms
24
+ ##
25
+ # The separator used in the original ISO 8601 string.
26
+ attr_reader :separator
27
+ ##
28
+ # @param [String] input The date pattern
29
+ def initialize(input)
30
+ @original = input
31
+
32
+ @atoms = atomize(input)
33
+ @date = ::Date.new(*@atoms)
34
+ rescue ArgumentError => error
35
+ raise ISO8601::Errors::RangeError, input
36
+ end
37
+ ##
38
+ # The calendar week number (1-53)
39
+ def week
40
+ @date.cweek
41
+ end
42
+ ##
43
+ # Forwards the date the given amount of days.
44
+ #
45
+ # @param [Numeric] days The days to add
46
+ #
47
+ # @return [ISO8601::Date] New date resulting of the addition
48
+ def +(days)
49
+ ISO8601::Date.new((@date + days).iso8601)
50
+ end
51
+ ##
52
+ # Backwards the date the given amount of days.
53
+ #
54
+ # @param [Numeric] days The days to remove
55
+ #
56
+ # @return [ISO8601::Date] New date resulting of the substraction
57
+ def -(days)
58
+ ISO8601::Date.new((@date - days).iso8601)
59
+ end
60
+ ##
61
+ # Converts self to an array of atoms.
62
+ def to_a
63
+ [year, month, day]
64
+ end
65
+
66
+ private
67
+ ##
68
+ # Splits the date component into valid atoms.
69
+ #
70
+ # Acceptable patterns:
71
+ #
72
+ # * YYYY
73
+ # * YYYY-MM but not YYYYMM
74
+ # * YYYY-MM-DD, YYYYMMDD
75
+ # * YYYY-Www, YYYYWdd
76
+ # * YYYY-Www-D, YYYYWddD
77
+ #
78
+ # @param [String] input
79
+ #
80
+ # @return [Array<Integer>]
81
+ def atomize(input)
82
+ _, year, separator, month, day = /^(?:
83
+ ([+-]?\d{4})(-?)(\d{2})\2(\d{2}) | # YYYY-MM-DD
84
+ ([+-]?\d{4})(-?)(\d{3}) | # YYYY-DDD
85
+ ([+-]?\d{4})(-)(\d{2}) | # YYYY-MM
86
+ ([+-]?\d{4}) # YYYY
87
+ )$/x.match(input).to_a.compact
88
+
89
+ if year.nil?
90
+ # Check if it's a Week date
91
+ _, year, separator, week, wday = /^(?:
92
+ ([+-]?\d{4})(-?)(W\d{2})\2(\d) | # YYYY-Www-D
93
+ ([+-]?\d{4})(-?)(W\d{2}) # YYYY-Www
94
+ )$/x.match(input).to_a.compact
95
+
96
+ unless week.nil?
97
+ d = ::Date.parse(input)
98
+ year = d.year
99
+ month = d.month
100
+ day = d.day
101
+ end
102
+ end
103
+
104
+ raise ISO8601::Errors::UnknownPattern.new(@original) if year.nil?
105
+
106
+ @separator = separator
107
+
108
+ return atomize_ordinal(year, month) if month && month.to_s.length == 3
109
+
110
+ [year, month, day].compact.map(&:to_i)
111
+ end
112
+ ##
113
+ # Parses an ordinal date (YYYY-DDD) and returns its atoms.
114
+ #
115
+ # @param [String] year in YYYY form.
116
+ # @param [String] day in DDD form.
117
+ #
118
+ # @return [Array<Integer>] date atoms
119
+ def atomize_ordinal(year, day)
120
+ date = ::Date.parse([year, day].join('-'))
121
+
122
+ [date.year, date.month, date.day]
123
+ end
124
+ end
125
+ end
@@ -4,124 +4,120 @@ module ISO8601
4
4
  ##
5
5
  # A DateTime representation
6
6
  #
7
- # @todo Review the pattern `201005`. It has to be `20-10-05` instead of `2010-05`.
8
- # The specification doesn't allow a YYYYMM. It should be always
9
- # YYYY-MM.
7
+ # @example
8
+ # dt = DateTime.new('2014-05-28T19:53Z')
9
+ # dt.year #=> 2014
10
10
  class DateTime
11
- attr_reader :century, :year, :month, :day, :hour, :minute, :second, :timezone
11
+ extend Forwardable
12
+
13
+ def_delegators(:@date_time,
14
+ :strftime, :to_time, :to_date, :to_datetime,
15
+ :year, :month, :day, :hour, :minute, :zone)
16
+
17
+ attr_reader :second
18
+
12
19
  ##
13
20
  # @param [String] date_time The datetime pattern
14
21
  def initialize(date_time)
15
- @dt = /^(?:
16
- (\d{2})(\d{2})? # Year. It can be either two digits (the century) or four digits (the full year)
17
- (?:
18
- (-)?(\d{2})
19
- )? # Month with an optional separator
20
- (?:
21
- (\3)?(\d{2}) # Day with an optional separator which is the same for the Month
22
- )?
23
- )? # Date
24
- (?:
25
- T(\d{2}) # Hour
26
- (?:
27
- (:)?(\d{2}) # Minute with an optional separator
28
- )?
29
- (?:
30
- (\8)?(\d{2}) # Second with an optional separator which is the same that for the Minute
31
- )? # Time
32
- (
33
- Z|([+-])
34
- (\d{2}) # Timezone hour
35
- (?:
36
- (\8)? # Separator which should be the same that for the Minute
37
- (\d{2}) # Timezone minute
38
- )?
39
- )? # Timezone
40
- )?
41
- $/x.match(date_time) or raise ISO8601::Errors::UnknownPattern.new(date_time)
42
-
43
- @date_time = date_time
44
- @time = @dt[7]
45
- @date_separator = @dt[3] == @dt[5] ? @dt[3] : nil
46
- @time_separator =
47
- if (!@dt[8].nil? and (!@dt[10].nil? and !@dt[11].nil?) and (!@dt[15].nil? and !@dt[16].nil?)) or
48
- (!@dt[8].nil? and (!@dt[10].nil? and !@dt[11].nil?) and @dt[16].nil?) or
49
- (!@dt[8].nil? and @dt[11].nil? and @dt[16].nil?)
50
- @dt[8]
51
- else
52
- nil
53
- end
54
- @century = @dt[1].to_i
55
- @year = @dt[2].nil? ? nil : (@dt[1] + @dt[2]).to_i
56
- @month = @dt[4].nil? ? nil : @dt[4].to_i
57
- @day = @dt[6].nil? ? nil : @dt[6].to_i
58
- @hour = @dt[7].nil? ? nil : @dt[7].to_i
59
- @minute = @dt[9].nil? ? nil : @dt[9].to_i
60
- @second = @dt[11].nil? ? nil : @dt[11].to_i
61
- @timezone = {
62
- :full => @dt[12].nil? ? (Time.now.gmt_offset / 3600) : (@dt[12] == "Z" ? 0 : @dt[12]),
63
- :sign => @dt[13],
64
- :hour => @dt[12].nil? ? (Time.now.gmt_offset / 3600) : (@dt[12] == "Z" ? 0 : @dt[14].to_i),
65
- :minute => (@dt[12].nil? or @dt[12] == "Z") ? 0 : @dt[13].to_i
66
- }
67
-
68
- valid_pattern?
69
- valid_range?
22
+ @original = date_time
23
+ @date_time = parse(date_time)
24
+ @second = @date_time.second + @date_time.second_fraction.to_f
70
25
  end
71
26
  ##
72
- # Returns the datetime string representation
73
- def to_s
74
- @date_time
27
+ # Addition
28
+ #
29
+ # @param [Numeric] seconds The seconds to add
30
+ def +(seconds)
31
+ moment = @date_time.to_time.localtime(zone) + seconds
32
+ format = moment.subsec.zero? ? "%Y-%m-%dT%H:%M:%S%:z" : "%Y-%m-%dT%H:%M:%S.%16N%:z"
33
+
34
+ ISO8601::DateTime.new(moment.strftime(format))
75
35
  end
76
36
  ##
77
- # Converts the object to a Time instance
37
+ # Substraction
78
38
  #
79
- # @return [Time] The object converted
80
- def to_time
81
- raise RangeError if @year.nil?
82
- if @month.nil?
83
- Time.utc(@year)
84
- elsif @day.nil?
85
- date = [@year, @month, '01'].join('-')
86
- Time.parse(date).getutc
87
- else
88
- Time.parse(@date_time).getutc
89
- end
39
+ # @param [Numeric] seconds The seconds to substract
40
+ def -(seconds)
41
+ moment = @date_time.to_time.localtime(zone) - seconds.round(1)
42
+ format = moment.subsec.zero? ? "%Y-%m-%dT%H:%M:%S%:z" : "%Y-%m-%dT%H:%M:%S.%2N%:z"
43
+
44
+ ISO8601::DateTime.new(moment.strftime(format))
90
45
  end
91
46
  ##
92
- # Addition
47
+ # Converts DateTime to a formated string
48
+ def to_s
49
+ format = @date_time.second_fraction.zero? ? "%Y-%m-%dT%H:%M:%S%:z" : "%Y-%m-%dT%H:%M:%S.%2N%:z"
50
+ @date_time.strftime(format)
51
+ end
52
+ ##
53
+ # Converts DateTime to an array of atoms.
54
+ def to_a
55
+ [year, month, day, hour, minute, second, zone]
56
+ end
57
+
58
+ private
59
+ ##
60
+ # Parses an ISO date time, where the date and the time components are
61
+ # optional.
93
62
  #
94
- # @param [ISO8601::DateTime] d The seconds to add
95
- def +(d)
96
- raise TypeError unless d.kind_of? Numeric
97
- ISO8601::DateTime.new((Time.utc(@year, @month, @day, @hour, @minute, @second) + d).iso8601)
63
+ # It enhances the parsing capabilities of the native DateTime.
64
+ #
65
+ # @param [String] date_time The ISO representation
66
+ def parse(date_time)
67
+ date, time = date_time.split('T')
68
+
69
+ date_components = parse_date(date)
70
+ time_components = Array(time && parse_time(time))
71
+ separators = [date_components.pop, time_components.pop]
72
+
73
+ raise ISO8601::Errors::UnknownPattern, @original unless valid_representation?(date_components, time_components)
74
+ raise ISO8601::Errors::UnknownPattern, @original unless valid_separators?(separators)
75
+
76
+ components = date_components + time_components
77
+
78
+ ::DateTime.new(*components.compact)
98
79
  end
99
80
  ##
100
- # Substraction
81
+ # Validates the date has the right pattern.
82
+ #
83
+ # Acceptable patterns: YYYY, YYYY-MM-DD, YYYYMMDD or YYYY-MM but not YYYYMM
101
84
  #
102
- # @param [ISO8601::DateTime] d The seconds to substract
103
- def -(d)
104
- raise TypeError unless d.kind_of? Numeric
105
- ISO8601::DateTime.new((Time.utc(@year, @month, @day, @hour, @minute, @second) - d).iso8601)
85
+ # @param [String] input A date component
86
+ #
87
+ # @return [Array<String, nil>]
88
+ def parse_date(input)
89
+ today = ::Date.today
90
+ return [today.year, today.month, today.day, :ignore] if input.empty?
91
+
92
+ date = ISO8601::Date.new(input)
93
+
94
+ date.atoms << date.separator
106
95
  end
107
- private
108
- def valid_pattern?
109
- if (@date_separator.nil? and !@time_separator.nil?) or
110
- (!@date_separator.nil? and !@time.nil? and @time_separator.nil? and !@minute.nil?) or
111
- (@year.nil? and !@month.nil?)
112
- raise ISO8601::Errors::UnknownPattern.new(@date_time)
113
- elsif (@year.nil? and @month.nil?)
114
- @year = (@century.to_s + "00").to_i
115
- end
116
- end
117
- def valid_range?
118
- if !month.nil? and (month < 1 or month > 12)
119
- raise RangeError
120
- elsif !day.nil? and (Time.parse(@date_time).month != month)
121
- raise RangeError
122
- end
123
- rescue ArgumentError
124
- raise RangeError
96
+ ##
97
+ # @return [Array<String, nil>]
98
+ def parse_time(input)
99
+ time = ISO8601::Time.new(input)
100
+
101
+ time.atoms << time.separator
102
+ end
103
+
104
+ def valid_separators?(separators)
105
+ separators = separators.compact
106
+
107
+ return true if separators.length == 1 || separators[0] == :ignore
108
+
109
+ unless separators.all?(&:empty?)
110
+ return false if (separators.first.length != separators.last.length)
125
111
  end
112
+ return true
113
+ end
114
+ ##
115
+ # If time is provided date must use a complete representation
116
+ def valid_representation?(date, time)
117
+ year, month, day = date
118
+ hour, minute, second = time
119
+
120
+ date.nil? || !(!year.nil? && (month.nil? || day.nil?) && !hour.nil?)
121
+ end
126
122
  end
127
123
  end