duranged 0.0.1

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: a2951d10f37ccb4455790939b928f13d81f99f66
4
+ data.tar.gz: 86b329f53282b75ce0fcbd6b1cb0c391e6808544
5
+ SHA512:
6
+ metadata.gz: 1e1561da9807d9564a2403c990f60ec5c297351bd1187f093c6fc83001440d1cd707eeb9a4155761bb79d629e4f5591156ce8852250fbb4fd5785a023873a56e
7
+ data.tar.gz: 776ca9e9111541289f471c0482433e86b19ed24eb1f2d47faa4a962f05b709422c20bef8a7f020c994bcc2bdd9812a024107146ff1537f443f36e4092907d723
@@ -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,6 @@
1
+ module Duranged
2
+ class Duration < Base
3
+ alias_attribute :duration, :value
4
+ alias_method :duration_string, :to_s
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Duranged
2
+ class Interval < Base
3
+ alias_attribute :interval, :value
4
+ alias_method :interval_string, :to_s
5
+ end
6
+ 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