iso8601 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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