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.
@@ -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