timespan 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,11 +1,14 @@
1
1
  source :rubygems
2
2
 
3
3
  gem 'chronic'
4
+ gem 'chronic_duration'
4
5
  gem 'spanner'
5
- gem 'ruby-duration'
6
+ gem 'ruby-duration', :git => 'git://github.com/kristianmandrup/ruby-duration.git'
6
7
 
7
8
  group :test, :development do
8
9
  gem "rspec", ">= 2.8.0"
10
+ gem 'rails', '~> 3.2'
11
+ # gem 'i18n'
9
12
  end
10
13
 
11
14
  group :development do
data/Gemfile.lock CHANGED
@@ -1,31 +1,88 @@
1
+ GIT
2
+ remote: git://github.com/kristianmandrup/ruby-duration.git
3
+ revision: 002f43a5ad5b6191ae5ee218e532176005279384
4
+ specs:
5
+ ruby-duration (2.1.4)
6
+ activesupport (>= 3.0.0)
7
+ i18n
8
+
1
9
  GEM
2
10
  remote: http://rubygems.org/
3
11
  specs:
12
+ actionmailer (3.2.3)
13
+ actionpack (= 3.2.3)
14
+ mail (~> 2.4.4)
15
+ actionpack (3.2.3)
16
+ activemodel (= 3.2.3)
17
+ activesupport (= 3.2.3)
18
+ builder (~> 3.0.0)
19
+ erubis (~> 2.7.0)
20
+ journey (~> 1.0.1)
21
+ rack (~> 1.4.0)
22
+ rack-cache (~> 1.2)
23
+ rack-test (~> 0.6.1)
24
+ sprockets (~> 2.1.2)
4
25
  activemodel (3.2.3)
5
26
  activesupport (= 3.2.3)
6
27
  builder (~> 3.0.0)
28
+ activerecord (3.2.3)
29
+ activemodel (= 3.2.3)
30
+ activesupport (= 3.2.3)
31
+ arel (~> 3.0.2)
32
+ tzinfo (~> 0.3.29)
33
+ activeresource (3.2.3)
34
+ activemodel (= 3.2.3)
35
+ activesupport (= 3.2.3)
7
36
  activesupport (3.2.3)
8
37
  i18n (~> 0.6)
9
38
  multi_json (~> 1.0)
10
- bson (1.6.2)
39
+ arel (3.0.2)
11
40
  builder (3.0.0)
12
41
  chronic (0.6.7)
42
+ chronic_duration (0.9.6)
43
+ numerizer (~> 0.1.1)
13
44
  diff-lcs (1.1.3)
45
+ erubis (2.7.0)
14
46
  git (1.2.5)
47
+ hike (1.2.1)
15
48
  i18n (0.6.0)
16
49
  jeweler (1.8.3)
17
50
  bundler (~> 1.0)
18
51
  git (>= 1.2.5)
19
52
  rake
20
53
  rdoc
54
+ journey (1.0.3)
21
55
  json (1.6.6)
22
- mongo (1.6.2)
23
- bson (~> 1.6.2)
24
- mongoid (2.4.9)
25
- activemodel (~> 3.1)
26
- mongo (~> 1.3)
27
- tzinfo (~> 0.3.22)
56
+ mail (2.4.4)
57
+ i18n (>= 0.4.0)
58
+ mime-types (~> 1.16)
59
+ treetop (~> 1.4.8)
60
+ mime-types (1.18)
28
61
  multi_json (1.3.2)
62
+ numerizer (0.1.1)
63
+ polyglot (0.3.3)
64
+ rack (1.4.1)
65
+ rack-cache (1.2)
66
+ rack (>= 0.4)
67
+ rack-ssl (1.3.2)
68
+ rack
69
+ rack-test (0.6.1)
70
+ rack (>= 1.0)
71
+ rails (3.2.3)
72
+ actionmailer (= 3.2.3)
73
+ actionpack (= 3.2.3)
74
+ activerecord (= 3.2.3)
75
+ activeresource (= 3.2.3)
76
+ activesupport (= 3.2.3)
77
+ bundler (~> 1.0)
78
+ railties (= 3.2.3)
79
+ railties (3.2.3)
80
+ actionpack (= 3.2.3)
81
+ activesupport (= 3.2.3)
82
+ rack-ssl (~> 1.3.2)
83
+ rake (>= 0.8.7)
84
+ rdoc (~> 3.4)
85
+ thor (~> 0.14.6)
29
86
  rake (0.9.2.2)
30
87
  rdoc (3.12)
31
88
  json (~> 1.4)
@@ -37,15 +94,20 @@ GEM
37
94
  rspec-expectations (2.9.1)
38
95
  diff-lcs (~> 1.1.3)
39
96
  rspec-mocks (2.9.0)
40
- ruby-duration (2.1.3)
41
- activesupport (>= 3.0.0)
42
- i18n
43
- mongoid (~> 2.4.0)
44
97
  simplecov (0.6.2)
45
98
  multi_json (~> 1.3)
46
99
  simplecov-html (~> 0.5.3)
47
100
  simplecov-html (0.5.3)
48
101
  spanner (0.0.2)
102
+ sprockets (2.1.2)
103
+ hike (~> 1.2)
104
+ rack (~> 1.0)
105
+ tilt (~> 1.1, != 1.3.0)
106
+ thor (0.14.6)
107
+ tilt (1.3.3)
108
+ treetop (1.4.10)
109
+ polyglot
110
+ polyglot (>= 0.3.1)
49
111
  tzinfo (0.3.33)
50
112
 
51
113
  PLATFORMS
@@ -54,9 +116,11 @@ PLATFORMS
54
116
  DEPENDENCIES
55
117
  bundler (>= 1.0.0)
56
118
  chronic
119
+ chronic_duration
57
120
  jeweler (>= 1.8.3)
121
+ rails (~> 3.2)
58
122
  rdoc (>= 3.12)
59
123
  rspec (>= 2.8.0)
60
- ruby-duration
124
+ ruby-duration!
61
125
  simplecov (>= 0.5)
62
126
  spanner
data/README.md CHANGED
@@ -1,30 +1,137 @@
1
1
  # Timespan
2
2
 
3
- Use TimeSpans in Ruby :)
3
+ Use Timespans in Ruby :)
4
4
 
5
- Will calculate time diff in milliseconds between to dates, then allow you to get the time difference in some time unit as a number.
5
+ Will calculate time diff between two dates, then allow you to get the time difference in some time unit as a number.
6
6
 
7
7
  ```ruby
8
- t = TimeSpan.new(:start => Date.today, :duration => 3.days.ago)
8
+ t = Timespan.new(:start => Date.today, :duration => 3.days.ago)
9
9
  t.to_days # => 3
10
10
  t.to_weeks # => 0
11
11
  t.to_secs # => 259200
12
12
  t.to_hours = 10800
13
13
 
14
- t = TimeSpan.new(:from => Date.today, :to => "6 weeks from now")
14
+ t = Timespan.new("2 days") # from today
15
15
 
16
- t = TimeSpan.new(:from => Date.today, :duration => "7 weeks 3 days")
17
- t = TimeSpan.new(:from => 2.days.ago, :duration => "5 months and 2 weeks")
18
- ``
16
+ t = Timespan.new("3 hrs").from(2.days.from_now)
17
+
18
+ t = Timespan.new(:from => Date.today, :to => "6 weeks from now")
19
+
20
+ t = Timespan.new(:from => Date.today, :duration => "7 weeks 3 days")
21
+ t = Timespan.new(:from => 2.days.ago, :duration => "5 months and 2 weeks")
22
+ ```
19
23
 
20
24
  See specs for more examples of usage
21
25
 
22
- ## TODO
26
+ ## Spanner
27
+
28
+ Internally Timespan uses Spanner to parse duration strings.
29
+
30
+ `Spanner.parse('23 hours 12 minutes')
31
+
32
+ ## Duration (ruby-duration)
33
+
34
+ ```ruby
35
+ Duration.new(100) => #<Duration: minutes=1, seconds=40, total=100>
36
+ Duration.new(:hours => 5, :minutes => 70) => #<Duration: hours=6, minutes=10, total=22200>
37
+
38
+ Duration.new(:weeks => 3, :days => 1).format("%w %~w and %d %~d") => "3 weeks and 1 day"
39
+ Duration.new(:weeks => 1, :days => 20).format("%w %~w and %d %~d") => "3 weeks and 6 days"
40
+ ```
41
+
42
+ Duration locale file
43
+
44
+ ```yaml
45
+ da:
46
+ ruby_duration:
47
+ second: sekond
48
+ seconds: sekonder
49
+ minute: minut
50
+ minutes: minutter
51
+ hour: time
52
+ hours: timer
53
+ day: dag
54
+ days: dage
55
+ week: uge
56
+ weeks: uges
57
+ month: måned
58
+ months: måneder
59
+ year: år
60
+ years: år
61
+ ```
62
+
63
+ Duration datatype for Mongoid
64
+
65
+ ```ruby
66
+ require 'duration/mongoid'
67
+
68
+ class MyModel
69
+ include Mongoid::Document
70
+ field :duration, type => Duration
71
+ end
72
+ ```
73
+
74
+ ## Timespan i18n
23
75
 
24
- * use Duration for duration calculations!
25
- * Calculate start_time and end_time based on duration if not set explicitly!
76
+ Timespan locale file
77
+
78
+ ```yaml
79
+ da:
80
+ timespan:
81
+ from: fra
82
+ to: til
83
+ lasting: der varer ialt
84
+ ```
85
+
86
+ ## Timespan for Mongoid
87
+
88
+ Custom Timespan datatype
89
+
90
+ ```ruby
91
+ require 'timespan/mongoid'
92
+
93
+ class MyModel
94
+ include Mongoid::Document
95
+ field :period, type => Timespan
96
+ end
97
+ ```
98
+
99
+ ## Chronic duration
100
+
101
+ Is used to parse duration strings if Spanner can't be handle it
102
+
103
+ `ChronicDuration.parse('4 minutes and 30 seconds')
104
+
105
+ ### Endure
106
+
107
+ Use the 'endure' gem based on the old "days_and_times".
108
+
109
+ See: [days_and_times](https://github.com/kristianmandrup/days_and_times)
110
+
111
+ Currently it also uses Duration, which conflicts with the 'ruby-duration' gem.
112
+
113
+ ```
114
+ 1.day #=> A duration of 1 day
115
+ 7.days #=> A duration of 7 days
116
+ 1.week #=> A duration of 1 week
117
+ 1.week - 2.days #=> A duration of 5 days
118
+ 1.week.from(Now()) #=> The time of 1 week from this moment
119
+ 1.week.from(Today()) #=> The time of 1 week from the beginning of today
120
+ 3.minutes.ago.until(7.minutes.from(Now())) #=> duration 3 minutes ago to 7 minutes from now
121
+ 3.minutes.ago.until(7.minutes.from(Now())) - 2.minutes #=> duration 3 minutes ago to 5 minutes from now
122
+ 4.weeks.from(2.days.from(Now())).until(8.weeks.from(Yesterday())) #=> A duration, starting in 4 weeks and 2 days, and ending 8 weeks from yesterday
123
+ 1.week - 1.second #=> A duration of 6 days, 23 hours, 59 minutes, and 59 seconds
124
+ 4.weeks / 2 #=> A duration of 2 weeks
125
+ 4.weeks / 2.weeks #=> The integer 2
126
+ 8.weeks.each {|week| ...} #=> Runs code for each week contained in the duration (of 8 weeks)
127
+ 8.weeks.starting(Now()).each {|week| ...} #=> Runs code for each week in the duration, but each week is also anchored to a starting time, in sequence through the duration.
128
+ 1.week.each {|week| ...} #=> Automatically chooses week as its iterator
129
+ 7.days.each {|day| ...} #=> Automatically chooses day as its iterator
130
+ 1.week.each_day {|day| ...} #=> Forcing the week to iterate through days
131
+ 1.week.each(10.hours) {|ten_hour_segment| ...} #=> Using a custom iterator of 10 hours. There would be 17 of them, but notice that the last iteration will only be 8 hours.
132
+ ``
26
133
 
27
- ## Contributing to timespan
134
+ ## Contributing to Timespan
28
135
 
29
136
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
30
137
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
@@ -34,7 +141,7 @@ See specs for more examples of usage
34
141
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
35
142
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
36
143
 
37
- == Copyright
144
+ ## Copyright
38
145
 
39
146
  Copyright (c) 2012 Kristian Mandrup. See LICENSE.txt for
40
147
  further details.
data/Rakefile CHANGED
@@ -17,8 +17,8 @@ Jeweler::Tasks.new do |gem|
17
17
  gem.name = "timespan"
18
18
  gem.homepage = "http://github.com/kristianmandrup/timespan"
19
19
  gem.license = "MIT"
20
- gem.summary = %Q{Use TimeSpans in ruby}
21
- gem.description = %Q{Easy to calculate time distance in different units}
20
+ gem.summary = %Q{Use timespans in ruby}
21
+ gem.description = %Q{Makes it easy to calculate time distance in different units}
22
22
  gem.email = "kmandrup@gmail.com"
23
23
  gem.authors = ["Kristian Mandrup"]
24
24
  # dependencies defined in Gemfile
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.2.0
@@ -0,0 +1,5 @@
1
+ da:
2
+ timespan:
3
+ from: fra
4
+ to: til
5
+ lasting: der varer ialt
data/lib/timespan.rb CHANGED
@@ -1,217 +1,211 @@
1
1
  require 'duration'
2
+ require 'chronic'
3
+ require 'chronic_duration'
2
4
  require 'spanner'
3
5
 
4
- class TimeSpan
5
- class ParseError < StandardError; end
6
+ require 'timespan/units'
7
+ require 'timespan/compare'
8
+ require 'timespan/printer'
9
+ require 'timespan/span'
6
10
 
7
- attr_reader :start_time, :end_time, :seconds
11
+ if defined?(Rails) && Rails::VERSION::STRING.to >= '3.1'
12
+ require 'duration/rails/engine'
13
+ end
14
+
15
+ class Timespan
16
+ include Span
17
+ include Printer
18
+ include Compare
19
+ include Units
20
+
21
+ class TimeParseError < StandardError; end
22
+
23
+ attr_reader :start_time, :end_time
8
24
 
9
25
  alias_method :start_date, :start_time
10
26
  alias_method :end_date, :end_time
11
27
 
28
+ START_KEYS = [:start, :from]
29
+ END_KEYS = [:to, :end]
30
+ DURATION_KEYS = [:duration, :lasting]
31
+
32
+ ALL_KEYS = START_KEYS + END_KEYS + DURATION_KEYS
33
+
12
34
  def initialize options = {}
13
35
  @is_new = true
14
36
 
15
- set_with_options options
37
+ case options
38
+ when Numeric, Duration, String
39
+ options = {:duration => options}
40
+ end
41
+
42
+ configure options
16
43
 
17
44
  @is_new = false
18
45
  end
19
46
 
20
- def start_time= start_time
21
- @start_time = start_time
22
- refresh!
47
+ def start_time= time
48
+ @start_time = convert_to_time time
49
+ unless is_new?
50
+ refresh!
51
+ add_dirty :start
52
+ calculate!
53
+ end
23
54
  end
24
55
  alias_method :start_date=, :start_time=
25
-
26
- def end_time= start_time
27
- @start_time = start_time
28
- refresh!
56
+
57
+ def from time
58
+ self.start_time = time
59
+ self
60
+ end
61
+
62
+ def end_time= time
63
+ @end_time = convert_to_time time
64
+ unless is_new?
65
+ add_dirty :end
66
+ refresh!
67
+ calculate!
68
+ end
29
69
  end
30
70
  alias_method :end_date=, :end_time=
31
71
 
32
- def seconds= seconds
33
- @seconds = seconds
34
- refresh!
72
+ def until time
73
+ self.end_time = time
74
+ self
35
75
  end
36
76
 
37
- def duration
38
- @duration ||= Duration.new(seconds)
39
- end
40
-
41
- def duration= duration
42
- @duration = case duration
43
- when Duration
44
- duration
45
- when Integer, Hash
46
- Duration.new duration
77
+ def convert_to_time time
78
+ case time
47
79
  when String
48
- begin
49
- Duration.new Spanner.parse(duration.gsub /and/, '')
50
- rescue Exception => e
51
- raise ParseError, "Internal error: Spanner couldn't parse String '#{duration}'"
52
- end
80
+ Chronic.parse(time)
81
+ when Date, DateTime
82
+ time.to_time
83
+ when Time
84
+ time
53
85
  else
54
- raise ArgumentError, "the duration option must be set to any of: #{valid_duration_types}"
55
- end
56
- refresh! unless is_new?
57
- end
58
-
59
- def to_s
60
- if duration
61
- "TimeSpan: from #{start_time} lasting #{duration} = #{seconds} secs" if start_time
62
- "TimeSpan: from #{end_time} to #{duration} before = #{seconds} secs" if end_time
63
- return
86
+ raise ArgumentError, "A valid time must be either a String, Date, Time or DateTime, was: #{time.inspect}"
64
87
  end
88
+ end
65
89
 
66
- if start_time && end_time
67
- "TimeSpan: #{start_time} to #{end_time} = #{seconds} secs"
68
- return
69
- end
90
+ protected
70
91
 
71
- if seconds
72
- "TimeSpan: #{seconds} seconds"
73
- end
92
+ def first_from keys, options = {}
93
+ keys.select {|key| options[key] }.first
74
94
  end
75
95
 
76
- include Comparable
77
-
78
- def <=> time
79
- raise ArgumentError, "Not a valid argument for timespan comparison, was #{time}" unless valid_compare?(time)
80
- case time
81
- when TimeSpan
82
- millis <=> time.seconds
83
- when Time
84
- millis <=> time.to_i
85
- when Date, DateTime
86
- time.to_time.to_i
87
- when Integer
88
- millis <=> time
89
- end
90
- end
91
-
92
- alias_method :to_secs, :seconds
93
- alias_method :to_seconds, :seconds
94
-
95
- def to_milliseconds
96
- @to_seconds ||= (seconds * 1000.0).round
97
- end
98
- alias_method :to_mils, :to_milliseconds
99
- alias_method :millis, :to_mils
100
- alias_method :milliseconds, :to_mils
101
-
102
- def to_minutes
103
- @to_minutes ||= (to_seconds / 60.0).round
104
- end
105
- alias_method :to_m, :to_minutes
106
- alias_method :to_mins, :to_minutes
107
- alias_method :minutes, :to_minutes
108
-
109
- def to_hours
110
- @to_hours ||= (to_minutes / 60.0).round
111
- end
112
- alias_method :to_h, :to_hours
113
- alias_method :hrs, :to_hours
114
- alias_method :hours, :to_hours
115
-
116
- def to_days
117
- @to_days ||= (to_hours / 24.0).round
118
- end
119
- alias_method :to_d, :to_days
120
- alias_method :days, :to_days
121
-
122
- def to_weeks
123
- @to_weeks ||= (to_days / 7.0).round
124
- end
125
- alias_method :to_w, :to_weeks
126
- alias_method :weeks, :to_days
127
-
128
- def to_months
129
- @to_months ||= (to_days / 30.0).round
130
- end
131
- alias_method :to_mon, :to_months
132
- alias_method :months, :to_months
133
-
134
- def to_years
135
- @to_years ||= (to_days.to_f / 365.25).round
136
- end
137
- alias_method :to_y, :to_years
138
- alias_method :years, :to_years
139
-
140
- def to_decades
141
- @to_decades ||= (to_years / 10.0).round
142
- end
143
- alias_method :decades, :to_decades
144
-
145
- def to_centuries
146
- @to_centuries ||= (to_decades / 10.0).round
147
- end
148
- alias_method :centuries, :to_centuries
149
-
150
- def units
151
- %w{seconds minutes hours days weeks months years}
152
- end
96
+ def configure options = {}
97
+ from = options[first_from START_KEYS, options]
98
+ to = options[first_from END_KEYS, options]
99
+ dur = options[first_from DURATION_KEYS, options]
153
100
 
154
- protected
101
+ self.duration = dur if dur
102
+ self.start_time = from if from
103
+ self.end_time = to if to
155
104
 
156
- def set_with_options options = {}
157
- case options
158
- when Hash
159
- duration = options[:duration]
160
- @start_time = options[:from] || options[:start]
161
- @end_time = options[:to] || options[:end]
162
- when Integer, String
163
- duration = options
164
- else
165
- raise ArgumentError, "Timespan must take Hash or Integer, was: #{options}"
166
- end
105
+ default_from_now! unless start_time || end_time
167
106
 
168
- set_seconds options
107
+ calculate_miss!
108
+ rescue Exception => e
109
+ calculate_miss!
110
+ validate!
169
111
  end
170
112
 
171
- def set_seconds options = nil
172
- set_seconds_opts(options)
113
+ def default_from_now!
114
+ self.start_time = Time.now
115
+ end
173
116
 
174
- unless @duration
175
- if @end_time && @start_time
176
- @seconds ||= (@end_time - @start_time).to_i
177
- end
178
- else
179
- @seconds ||= @duration.total
180
- end
117
+ def validate!
118
+ raise ArgumentError, "#{valid_requirement}, was: #{current_config}" unless valid?
181
119
  end
182
120
 
183
- def set_seconds_opts options = {}
184
- case options
185
- when Integer
186
- @seconds = options
187
- when Hash
188
- @seconds = options[:seconds] if options[:seconds]
189
- self.duration = options[:duration] if options[:duration]
190
- end
121
+ def valid_requirement
122
+ "Timespan must take a :start and :end time or any of :start and :end time and a :duration"
123
+ end
124
+
125
+ def current_config
126
+ "end time: #{end_time}, start time: #{start_time}, duration: #{duration}"
127
+ end
128
+
129
+ def valid?
130
+ (end_time && start_time) || (end_time || start_time && duration)
191
131
  end
192
132
 
193
133
  def is_new?
194
134
  @is_new
195
135
  end
196
136
 
197
- # reset all stored instance vars for units
198
- def refresh!
199
- units.each do |unit|
200
- var_name = :"@#{unit}"
201
- instance_variable_set var_name, nil
202
- end
203
- set_seconds
137
+ def dirty
138
+ @dirty ||= []
139
+ end
140
+
141
+ def add_dirty type
142
+ reset_dirty if dirty.size > 2
143
+ dirty << type
144
+ end
145
+
146
+ def reset_dirty
147
+ @dirty = []
148
+ end
149
+
150
+ def dirty? type
151
+ dirty.include? type
152
+ end
153
+
154
+ def calculate!
155
+ set_duration unless dirty? :duration
156
+ set_start_time unless dirty? :start
157
+ set_end_time unless dirty? :end
158
+ end
159
+
160
+ def calculate_miss!
161
+ set_end_time_miss
162
+ set_start_time_miss
163
+ set_duration_miss
164
+ set_end_time_miss
165
+ set_start_time_miss
166
+ end
167
+
168
+ def set_end_time_miss
169
+ set_end_time if missing_end_time?
170
+ end
171
+
172
+ def set_end_time
173
+ self.end_time = start_time - duration.total
174
+ end
175
+
176
+ def set_start_time_miss
177
+ set_start_time if missing_start_time?
178
+ end
179
+
180
+ def set_start_time
181
+ self.start_time = end_time - duration.total
182
+ end
183
+
184
+ def set_duration_miss
185
+ set_duration if missing_duration?
186
+ end
187
+
188
+ def set_duration
189
+ self.duration = end_time - start_time
204
190
  end
205
191
 
206
- def valid_duration_types
207
- [Duration, String, Integer, Hash]
192
+ def missing_end_time?
193
+ start_time && duration && !end_time
208
194
  end
209
195
 
210
- def valid_compare? time
211
- valid_compare_types.any? {|type| time.kind_of? type }
196
+ def missing_start_time?
197
+ end_time && duration && !start_time
212
198
  end
213
199
 
214
- def valid_compare_types
215
- [TimeSpan, Time, Date, DateTime, Integer]
200
+ def missing_duration?
201
+ start_time && end_time && !duration
202
+ end
203
+
204
+ # reset all stored instance vars for units
205
+ def refresh!
206
+ units.each do |unit|
207
+ var_name = :"@#{unit}"
208
+ instance_variable_set var_name, nil
209
+ end
216
210
  end
217
211
  end