duranged 0.0.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.
- checksums.yaml +7 -0
- data/lib/duranged.rb +45 -0
- data/lib/duranged/base.rb +182 -0
- data/lib/duranged/duration.rb +6 -0
- data/lib/duranged/interval.rb +6 -0
- data/lib/duranged/occurrence.rb +124 -0
- data/lib/duranged/range.rb +111 -0
- data/lib/duranged/version.rb +3 -0
- data/spec/duranged/base_spec.rb +6 -0
- data/spec/duranged/duration_spec.rb +14 -0
- data/spec/duranged/interval_spec.rb +14 -0
- data/spec/duranged/occurrence_spec.rb +245 -0
- data/spec/duranged/range_spec.rb +324 -0
- data/spec/duranged_spec.rb +15 -0
- data/spec/shared/base_examples.rb +224 -0
- data/spec/spec_helper.rb +78 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a2951d10f37ccb4455790939b928f13d81f99f66
|
4
|
+
data.tar.gz: 86b329f53282b75ce0fcbd6b1cb0c391e6808544
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e1561da9807d9564a2403c990f60ec5c297351bd1187f093c6fc83001440d1cd707eeb9a4155761bb79d629e4f5591156ce8852250fbb4fd5785a023873a56e
|
7
|
+
data.tar.gz: 776ca9e9111541289f471c0482433e86b19ed24eb1f2d47faa4a962f05b709422c20bef8a7f020c994bcc2bdd9812a024107146ff1537f443f36e4092907d723
|
data/lib/duranged.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/enumerable'
|
3
|
+
require 'active_support/core_ext/object'
|
4
|
+
require 'active_support/core_ext/numeric'
|
5
|
+
require 'active_support/core_ext/time'
|
6
|
+
require 'canfig'
|
7
|
+
require 'chronic_duration'
|
8
|
+
require 'duranged/base'
|
9
|
+
require 'duranged/duration'
|
10
|
+
require 'duranged/interval'
|
11
|
+
require 'duranged/occurrence'
|
12
|
+
require 'duranged/range'
|
13
|
+
|
14
|
+
module Duranged
|
15
|
+
include Canfig::Module
|
16
|
+
|
17
|
+
configure do |config|
|
18
|
+
config.formats = Canfig.new(date: '%b. %-d %Y', time: '%-l:%M%P')
|
19
|
+
config.logger = ActiveSupport::Logger.new(STDOUT)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.formats
|
23
|
+
configuration.formats
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
configuration.logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.interval(interval)
|
31
|
+
Interval.new(interval)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.duration(duration)
|
35
|
+
Duration.new(duration)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.range(start_at, end_at_or_duration)
|
39
|
+
Range.new(start_at, end_at_or_duration)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.occurrence(occurrences=1, interval=nil, duration=nil)
|
43
|
+
Occurrence.new(occurrences, interval, duration)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Duranged
|
2
|
+
class Base
|
3
|
+
include Comparable
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
PARTS = [:years, :months, :weeks, :days_after_weeks, :days, :hours, :minutes, :seconds]
|
7
|
+
FORMATTERS = { 's' => -> (pad,with) { pad_value seconds, pad, with },
|
8
|
+
'm' => -> (pad,with) { pad_value minutes, pad, with },
|
9
|
+
'h' => -> (pad,with) { pad_value hours, pad, with },
|
10
|
+
'd' => -> (pad,with) { pad_value days, pad, with },
|
11
|
+
'D' => -> (pad,with) { pad_value days_after_weeks, pad, with },
|
12
|
+
'w' => -> (pad,with) { pad_value weeks, pad, with },
|
13
|
+
'M' => -> (pad,with) { pad_value months, pad, with },
|
14
|
+
'y' => -> (pad,with) { pad_value years, pad, with } }
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def dump(obj)
|
18
|
+
return if obj.nil?
|
19
|
+
obj.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
def load(json)
|
23
|
+
value = JSON.load(json)
|
24
|
+
new(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(value)
|
29
|
+
if value.is_a?(Hash)
|
30
|
+
@value = parse_hash(value)
|
31
|
+
elsif value.is_a?(String) && value.to_i.to_s != value
|
32
|
+
@value = ChronicDuration.parse(value, keep_zero: true).to_i
|
33
|
+
else
|
34
|
+
@value = value.to_i
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def years
|
39
|
+
(value.to_f / 60 / 60 / 24 / 365.25).to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
def months
|
43
|
+
((value - years.years) / 60 / 60 / 24 / 30).floor
|
44
|
+
end
|
45
|
+
|
46
|
+
def weeks
|
47
|
+
(((value - months.months - years.years) / 60 / 60 / 24).floor / 7).floor
|
48
|
+
end
|
49
|
+
|
50
|
+
def days_after_weeks
|
51
|
+
((value - weeks.weeks - months.months - years.years) / 60 / 60 / 24).floor
|
52
|
+
end
|
53
|
+
|
54
|
+
def days
|
55
|
+
((value - months.months - years.years) / 60 / 60 / 24).floor
|
56
|
+
end
|
57
|
+
|
58
|
+
def hours
|
59
|
+
((value - days.days - months.months - years.years) / 60 / 60).floor
|
60
|
+
end
|
61
|
+
|
62
|
+
def minutes
|
63
|
+
((value - hours.hours - days.days - months.months - years.years) / 60).floor
|
64
|
+
end
|
65
|
+
|
66
|
+
def seconds
|
67
|
+
(value - minutes.minutes - hours.hours - days.days - months.months - years.years).floor
|
68
|
+
end
|
69
|
+
|
70
|
+
def +(other)
|
71
|
+
if other.is_a?(Duration) || other.is_a?(Interval)
|
72
|
+
Duranged.logger.warn "Warning: You are adding a #{other.class.name} to a #{self.class.name}, which will result in a #{self.class.name}. If you would like a #{other.class.name} object to be returned you must add your #{self.class.name} to your #{other.class.name} instead." if other.is_a?(Range)
|
73
|
+
self.class.new(value + other.value)
|
74
|
+
elsif other.is_a?(Integer)
|
75
|
+
self.class.new(value + other)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "value must be an Integer, Duranged::Duration or Duranged::Interval"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def -(other)
|
82
|
+
if other.is_a?(Duration) || other.is_a?(Interval)
|
83
|
+
Duranged.logger.warn "Warning: You are subtracting a #{other.class.name} from a #{self.class.name}, which will result in a #{self.class.name}. If you would like a #{other.class.name} object to be returned you must subtract your #{self.class.name} from your #{other.class.name} instead." if other.is_a?(Range)
|
84
|
+
self.class.new(value - other.value)
|
85
|
+
elsif other.is_a?(Integer)
|
86
|
+
self.class.new(value - other)
|
87
|
+
else
|
88
|
+
raise ArgumentError, "value must be an Integer, Duranged::Duration or Duranged::Interval"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def as_json(options=nil)
|
93
|
+
value
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_h
|
97
|
+
PARTS.map do |part|
|
98
|
+
[part, send(part)]
|
99
|
+
end.to_h
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
ChronicDuration.output(value, format: :long, joiner: ', ').to_s
|
104
|
+
end
|
105
|
+
|
106
|
+
def strfdur(format)
|
107
|
+
str = format.to_s
|
108
|
+
|
109
|
+
# :years(%Y years, ):months(%N months, )%D :days
|
110
|
+
PARTS.each do |part|
|
111
|
+
while matches = str.match(/:(#{part})(\((.+)\))?/i) do
|
112
|
+
if matches[3]
|
113
|
+
# only replaces if the value is > 0, otherwise blank
|
114
|
+
matched = ''
|
115
|
+
depth = 0
|
116
|
+
matches[3].chars.to_a.each do |char|
|
117
|
+
depth += 1 if char == '('
|
118
|
+
depth -= 1 if char == ')'
|
119
|
+
break if depth == -1
|
120
|
+
matched += char
|
121
|
+
end
|
122
|
+
value = send(part) > 0 ? strfdur(matched.dup) : ''
|
123
|
+
str.gsub!(":#{part}(#{matched})", value)
|
124
|
+
else
|
125
|
+
# if no nested format was passed, replace with a singular
|
126
|
+
# or plural part name as appropriate
|
127
|
+
value = send(part) == 1 ? matches[1].to_s.singularize : matches[1].to_s
|
128
|
+
str.gsub!(matches[0], value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
FORMATTERS.each do |conversion, block|
|
134
|
+
while matches = str.match(/%([-_])?([0-9]+)?(#{conversion})/) do
|
135
|
+
pad_with = matches[1] == '_' ? :space : :zero
|
136
|
+
value = instance_exec(matches[2] || 2, pad_with, &block)
|
137
|
+
value = value.to_i.to_s.lstrip if matches[1] == '-'
|
138
|
+
|
139
|
+
str.gsub!(matches[0], value)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
str
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_i
|
147
|
+
value
|
148
|
+
end
|
149
|
+
|
150
|
+
def <=>(other)
|
151
|
+
if value < other.to_i
|
152
|
+
-1
|
153
|
+
elsif value > other.to_i
|
154
|
+
1
|
155
|
+
else
|
156
|
+
0
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def call
|
161
|
+
value
|
162
|
+
end
|
163
|
+
|
164
|
+
protected
|
165
|
+
|
166
|
+
def parse_hash(hash)
|
167
|
+
hash.sum { |k,v| v.to_i.send(k.to_sym) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def pad_value(value, pad=2, with=:zero)
|
171
|
+
send("#{with}_pad".to_sym, value, pad)
|
172
|
+
end
|
173
|
+
|
174
|
+
def zero_pad(value, pad=2)
|
175
|
+
"%0#{pad}d" % value
|
176
|
+
end
|
177
|
+
|
178
|
+
def space_pad(value, pad=2)
|
179
|
+
"%#{pad}d" % value
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Duranged
|
2
|
+
class Occurrence
|
3
|
+
attr_reader :occurrences, :interval, :duration, :range
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def dump(obj)
|
7
|
+
return if obj.nil?
|
8
|
+
obj.to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(json)
|
12
|
+
hash = JSON.load(json)
|
13
|
+
args = [hash['occurrences'], hash['interval'], hash['duration']]
|
14
|
+
args.concat [hash['range']['start_at'].to_datetime, hash['range']['end_at'].to_datetime] if hash.key?('range')
|
15
|
+
new(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(occurrences=1, interval=nil, duration=nil, range_start=nil, range_end_or_duration=nil)
|
20
|
+
if occurrences.class <= Enumerable
|
21
|
+
@occurrences = occurrences.count
|
22
|
+
else
|
23
|
+
@occurrences = occurrences.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
@interval = Interval.new(interval) if interval
|
27
|
+
@duration = Duration.new(duration) if duration
|
28
|
+
if range_start.present?
|
29
|
+
if range_end_or_duration.nil?
|
30
|
+
range_end_or_duration = range_start.to_datetime
|
31
|
+
range_end_or_duration = range_end_or_duration + (@interval.value * (occurrences - 1)).seconds if @interval
|
32
|
+
range_end_or_duration = range_end_or_duration + (@duration.value * occurrences).seconds if @duration
|
33
|
+
end
|
34
|
+
@range = Range.new(range_start, range_end_or_duration)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def occurrences_string
|
39
|
+
case occurrences
|
40
|
+
when 0
|
41
|
+
"never"
|
42
|
+
when 1
|
43
|
+
"once"
|
44
|
+
when 2
|
45
|
+
"twice"
|
46
|
+
else
|
47
|
+
"#{occurrences} times"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def as_json(options=nil)
|
52
|
+
hash = { occurrences: occurrences }
|
53
|
+
hash[:interval] = interval.as_json(options) if interval
|
54
|
+
hash[:duration] = duration.as_json(options) if duration
|
55
|
+
hash[:range] = range.as_json(options) if range
|
56
|
+
hash
|
57
|
+
end
|
58
|
+
alias_method :to_h, :as_json
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
return occurrences_string if occurrences == 0
|
62
|
+
|
63
|
+
str = [occurrences_string]
|
64
|
+
|
65
|
+
if duration.present? && duration > 0
|
66
|
+
str << "for"
|
67
|
+
str << duration.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
if interval.present? && interval > 0
|
71
|
+
str << "every"
|
72
|
+
str << interval.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
if range.present?
|
76
|
+
if range.same_day?
|
77
|
+
str << range.strfrange("on :start_at(#{Duranged.configuration.formats.date}) between :start_at(#{Duranged.configuration.formats.time}) and :end_at(#{Duranged.configuration.formats.time})")
|
78
|
+
else
|
79
|
+
str << range.strfrange("between :start_at(#{Duranged.configuration.formats.date} #{Duranged.configuration.formats.time}) and :end_at(#{Duranged.configuration.formats.date} #{Duranged.configuration.formats.time})")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
str.join(' ')
|
84
|
+
end
|
85
|
+
|
86
|
+
def strfocc(format)
|
87
|
+
str = format.to_s
|
88
|
+
|
89
|
+
while matches = str.match(/:occurrence[s]?/) do
|
90
|
+
str.gsub!(matches[0], occurrences.to_s)
|
91
|
+
end
|
92
|
+
|
93
|
+
if range
|
94
|
+
while matches = str.match(/:range(\((.+)\))?/) do
|
95
|
+
if matches[2]
|
96
|
+
matched = ''
|
97
|
+
depth = 0
|
98
|
+
matches[2].chars.to_a.each do |char|
|
99
|
+
depth += 1 if char == '('
|
100
|
+
depth -= 1 if char == ')'
|
101
|
+
break if depth == -1
|
102
|
+
matched += char
|
103
|
+
end
|
104
|
+
value = range.strfrange(matched.dup)
|
105
|
+
str.gsub!(":range(#{matched})", value)
|
106
|
+
else
|
107
|
+
value = range.to_s
|
108
|
+
str.gsub!(matches[0], value)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if duration || interval
|
114
|
+
while matches = str.match(/:(duration|interval)(\(([^\)]+)\))/) do
|
115
|
+
matched = send(matches[1])
|
116
|
+
value = matched.nil? ? '' : matched.strfdur(matches[3])
|
117
|
+
str.gsub!(matches[0], value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
str
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Duranged
|
2
|
+
class Range < Duration
|
3
|
+
class << self
|
4
|
+
def load(json)
|
5
|
+
hash = JSON.load(json)
|
6
|
+
new(hash['start_at'].to_datetime, hash['end_at'].to_datetime)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Range.new(Time.now, 1.hour)
|
11
|
+
# Range.new(Time.now, 1.hour.from_now)
|
12
|
+
# Range.new(Time.now, (Time.now + 1.hour))
|
13
|
+
# Range.new(1.hour) # start_at defaults to now
|
14
|
+
# Range.new(1.hour.from_now) # start_at defaults to now
|
15
|
+
# Range.new(Time.now + 1.hour) # start_at defaults to now
|
16
|
+
def initialize(*args)
|
17
|
+
if args.length == 2
|
18
|
+
start_at, end_at_or_duration = *args
|
19
|
+
elsif args.length == 1
|
20
|
+
start_at = DateTime.now
|
21
|
+
end_at_or_duration = args.first
|
22
|
+
else
|
23
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)"
|
24
|
+
end
|
25
|
+
|
26
|
+
@start_at = start_at.to_datetime
|
27
|
+
if end_at_or_duration.is_a?(Integer) || end_at_or_duration.is_a?(ActiveSupport::Duration)
|
28
|
+
super(end_at_or_duration.to_i)
|
29
|
+
@end_at = (@start_at + duration.seconds).to_datetime
|
30
|
+
elsif end_at_or_duration.is_a?(Hash) || end_at_or_duration.is_a?(String)
|
31
|
+
super(end_at_or_duration)
|
32
|
+
@end_at = (@start_at + duration.seconds).to_datetime
|
33
|
+
else
|
34
|
+
@end_at = end_at_or_duration.to_datetime
|
35
|
+
super(@end_at.to_i - @start_at.to_i)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_at(format=nil)
|
40
|
+
format.nil? ? @start_at : @start_at.strftime(format).strip
|
41
|
+
end
|
42
|
+
alias_method :start_date, :start_at
|
43
|
+
alias_method :start_time, :start_at
|
44
|
+
|
45
|
+
def end_at(format=nil)
|
46
|
+
format.nil? ? @end_at : @end_at.strftime(format).strip
|
47
|
+
end
|
48
|
+
alias_method :end_date, :end_at
|
49
|
+
alias_method :end_time, :end_at
|
50
|
+
|
51
|
+
def +(other)
|
52
|
+
if other.is_a?(Duration) || other.is_a?(Interval)
|
53
|
+
self.class.new(start_at, value + other.value)
|
54
|
+
elsif other.is_a?(Integer)
|
55
|
+
self.class.new(start_at, value + other)
|
56
|
+
else
|
57
|
+
raise ArgumentError, "value must be an Integer, Duranged::Duration or Duranged::Interval"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def -(other)
|
62
|
+
if other.is_a?(Duration) || other.is_a?(Interval)
|
63
|
+
self.class.new(start_at, value - other.value)
|
64
|
+
elsif other.is_a?(Integer)
|
65
|
+
self.class.new(start_at, value - other)
|
66
|
+
else
|
67
|
+
raise ArgumentError, "value must be an Integer, Duranged::Duration or Duranged::Interval"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_duration
|
72
|
+
Duration.new(duration)
|
73
|
+
end
|
74
|
+
|
75
|
+
def as_json(options=nil)
|
76
|
+
{ start_at: start_at.as_json,
|
77
|
+
end_at: end_at.as_json }
|
78
|
+
end
|
79
|
+
alias_method :to_h, :as_json
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
if same_day?
|
83
|
+
"#{start_at("#{Duranged.formats.date} #{Duranged.formats.time}")} to #{end_at("#{Duranged.formats.time}")}"
|
84
|
+
else
|
85
|
+
"#{start_at("#{Duranged.formats.date} #{Duranged.formats.time}")} (#{super()})"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def strfrange(format)
|
90
|
+
str = format.to_s
|
91
|
+
|
92
|
+
while matches = str.match(/:(start_at|end_at|)\((([^\)]+))\)/) do
|
93
|
+
str.gsub!(matches[0], send(matches[1], matches[2]))
|
94
|
+
end
|
95
|
+
|
96
|
+
while matches = str.match(/:duration\(([^\)]+)\)/) do
|
97
|
+
str.gsub!(matches[0], strfdur(matches[1]))
|
98
|
+
end
|
99
|
+
|
100
|
+
str
|
101
|
+
end
|
102
|
+
|
103
|
+
def call
|
104
|
+
to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
def same_day?
|
108
|
+
start_at('%j') == end_at('%j')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|