biz 0.0.1 → 1.0.0

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,81 @@
1
+ module Biz
2
+ class DayOfWeek
3
+
4
+ SYMBOLS = [:sun, :mon, :tue, :wed, :thu, :fri, :sat]
5
+
6
+ def self.from_time(time)
7
+ DAYS.fetch(time.wday)
8
+ end
9
+
10
+ def self.from_symbol(symbol)
11
+ DAYS.fetch(SYMBOLS.index(symbol))
12
+ end
13
+
14
+ def self.first
15
+ DAYS.first
16
+ end
17
+
18
+ def self.last
19
+ DAYS.last
20
+ end
21
+
22
+ class << self
23
+
24
+ alias_method :from_date, :from_time
25
+
26
+ end
27
+
28
+ attr_reader :wday
29
+
30
+ def initialize(wday)
31
+ @wday = Integer(wday)
32
+ end
33
+
34
+ def contains?(week_time)
35
+ minutes.cover?(week_time)
36
+ end
37
+
38
+ def start_minute
39
+ wday * Time::MINUTES_IN_DAY
40
+ end
41
+
42
+ def end_minute
43
+ start_minute + Time::MINUTES_IN_DAY
44
+ end
45
+
46
+ def minutes
47
+ start_minute..end_minute
48
+ end
49
+
50
+ def week_minute(day_minute)
51
+ start_minute + day_minute
52
+ end
53
+
54
+ def day_minute(week_minute)
55
+ (week_minute - 1) % Time::MINUTES_IN_DAY + 1
56
+ end
57
+
58
+ def symbol
59
+ SYMBOLS.fetch(wday)
60
+ end
61
+
62
+ DAYS = [
63
+ SUNDAY = new(0),
64
+ MONDAY = new(1),
65
+ TUESDAY = new(2),
66
+ WEDNESDAY = new(3),
67
+ THURSDAY = new(4),
68
+ FRIDAY = new(5),
69
+ SATURDAY = new(6)
70
+ ]
71
+
72
+ WEEKDAYS = [
73
+ MONDAY,
74
+ TUESDAY,
75
+ WEDNESDAY,
76
+ THURSDAY,
77
+ FRIDAY
78
+ ]
79
+
80
+ end
81
+ end
@@ -0,0 +1,95 @@
1
+ module Biz
2
+ class DayTime
3
+
4
+ TIMESTAMP_FORMAT = '%02d:%02d'
5
+ TIMESTAMP_PATTERN = /(?<hour>\d{2}):(?<minute>\d{2})/
6
+
7
+ include Comparable
8
+
9
+ extend Forwardable
10
+
11
+ def self.from_hour(hour)
12
+ new(hour * Time::MINUTES_IN_HOUR)
13
+ end
14
+
15
+ def self.from_timestamp(timestamp)
16
+ timestamp.match(TIMESTAMP_PATTERN) { |match|
17
+ new(match[:hour].to_i * Time::MINUTES_IN_HOUR + match[:minute].to_i)
18
+ }
19
+ end
20
+
21
+ def self.midnight
22
+ MIDNIGHT
23
+ end
24
+
25
+ def self.noon
26
+ NOON
27
+ end
28
+
29
+ def self.endnight
30
+ ENDNIGHT
31
+ end
32
+
33
+ class << self
34
+
35
+ alias_method :am, :midnight
36
+
37
+ alias_method :pm, :noon
38
+
39
+ end
40
+
41
+ attr_reader :day_minute
42
+
43
+ delegate strftime: :day_time
44
+
45
+ delegate %i[
46
+ to_i
47
+ to_int
48
+ ] => :day_minute
49
+
50
+ def initialize(day_minute)
51
+ @day_minute = Integer(day_minute)
52
+ end
53
+
54
+ def hour
55
+ day_minute / Time::MINUTES_IN_HOUR
56
+ end
57
+
58
+ def minute
59
+ day_minute % Time::MINUTES_IN_HOUR
60
+ end
61
+
62
+ def timestamp
63
+ format(TIMESTAMP_FORMAT, hour, minute)
64
+ end
65
+
66
+ def coerce(other)
67
+ [self.class.new(other), self]
68
+ end
69
+
70
+ protected
71
+
72
+ def <=>(other)
73
+ return nil unless other.respond_to?(:to_i)
74
+
75
+ day_minute <=> other.to_i
76
+ end
77
+
78
+ private
79
+
80
+ def day_time
81
+ ::Time.new(
82
+ Date::EPOCH.year,
83
+ Date::EPOCH.month,
84
+ Date::EPOCH.mday,
85
+ hour,
86
+ minute
87
+ )
88
+ end
89
+
90
+ MIDNIGHT = from_hour(0)
91
+ NOON = from_hour(12)
92
+ ENDNIGHT = from_hour(24)
93
+
94
+ end
95
+ end
@@ -0,0 +1,100 @@
1
+ module Biz
2
+ class Duration
3
+
4
+ UNITS = Set.new(%i[second seconds minute minutes hour hours day days])
5
+
6
+ include Equalizer.new(:seconds)
7
+ include Comparable
8
+
9
+ extend Forwardable
10
+
11
+ class << self
12
+
13
+ def with_unit(scalar, unit)
14
+ unless UNITS.include?(unit)
15
+ fail ArgumentError, 'The unit is not supported.'
16
+ end
17
+
18
+ public_send(unit, scalar)
19
+ end
20
+
21
+ def seconds(seconds)
22
+ new(seconds)
23
+ end
24
+
25
+ alias_method :second, :seconds
26
+
27
+ def minutes(minutes)
28
+ new(minutes * Time::MINUTE)
29
+ end
30
+
31
+ alias_method :minute, :minutes
32
+
33
+ def hours(hours)
34
+ new(hours * Time::HOUR)
35
+ end
36
+
37
+ alias_method :hour, :hours
38
+
39
+ def days(days)
40
+ new(days * Time::DAY)
41
+ end
42
+
43
+ alias_method :day, :days
44
+
45
+ end
46
+
47
+ attr_reader :seconds
48
+
49
+ delegate to_i: :seconds
50
+
51
+ def initialize(seconds)
52
+ @seconds = Integer(seconds)
53
+ end
54
+
55
+ def in_seconds
56
+ seconds
57
+ end
58
+
59
+ def in_minutes
60
+ seconds / Time::MINUTE
61
+ end
62
+
63
+ def in_hours
64
+ seconds / Time::HOUR
65
+ end
66
+
67
+ def in_days
68
+ seconds / Time::DAY
69
+ end
70
+
71
+ def +(other)
72
+ self.class.new(seconds + other.seconds)
73
+ end
74
+
75
+ def -(other)
76
+ self.class.new(seconds - other.seconds)
77
+ end
78
+
79
+ def positive?
80
+ seconds > 0
81
+ end
82
+
83
+ def abs
84
+ self.class.new(seconds.abs)
85
+ end
86
+
87
+ def coerce(other)
88
+ [self.class.new(other), self]
89
+ end
90
+
91
+ protected
92
+
93
+ def <=>(other)
94
+ return nil unless other.respond_to?(:to_i)
95
+
96
+ seconds <=> other.to_i
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,26 @@
1
+ module Biz
2
+ class Holiday
3
+
4
+ include Equalizer.new(:date, :time_zone)
5
+
6
+ attr_reader :date,
7
+ :time_zone
8
+
9
+ def initialize(date, time_zone)
10
+ @date = date
11
+ @time_zone = time_zone
12
+ end
13
+
14
+ def contains?(time)
15
+ date == Time.new(time_zone).local(time).to_date
16
+ end
17
+
18
+ def to_time_segment
19
+ TimeSegment.new(
20
+ Time.new(time_zone).on_date(date, DayTime.midnight),
21
+ Time.new(time_zone).on_date(date, DayTime.endnight)
22
+ )
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ module Biz
2
+ class Interval
3
+
4
+ include Equalizer.new(:start_time, :end_time, :time_zone)
5
+
6
+ attr_reader :start_time,
7
+ :end_time,
8
+ :time_zone
9
+
10
+ def initialize(start_time, end_time, time_zone)
11
+ @start_time = start_time
12
+ @end_time = end_time
13
+ @time_zone = time_zone
14
+ end
15
+
16
+ def endpoints
17
+ [start_time, end_time]
18
+ end
19
+
20
+ def contains?(time)
21
+ (start_time...end_time).cover?(
22
+ WeekTime.from_time(Time.new(time_zone).local(time))
23
+ )
24
+ end
25
+
26
+ def to_time_segment(week)
27
+ TimeSegment.new(
28
+ *endpoints.map { |endpoint|
29
+ Time.new(time_zone).during_week(week, endpoint)
30
+ }
31
+ )
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ module Biz
2
+ class Periods
3
+
4
+ attr_reader :schedule
5
+
6
+ def initialize(schedule)
7
+ @schedule = schedule
8
+ end
9
+
10
+ def after(origin)
11
+ After.new(schedule, origin)
12
+ end
13
+
14
+ def before(origin)
15
+ Before.new(schedule, origin)
16
+ end
17
+
18
+ end
19
+ end
20
+
21
+ require 'biz/periods/abstract'
22
+
23
+ require 'biz/periods/after'
24
+ require 'biz/periods/before'
@@ -0,0 +1,49 @@
1
+ module Biz
2
+ class Periods
3
+ class Abstract < Enumerator::Lazy
4
+
5
+ extend Forwardable
6
+
7
+ attr_reader :schedule,
8
+ :origin
9
+
10
+ def initialize(schedule, origin)
11
+ @schedule = schedule
12
+ @origin = origin
13
+
14
+ super(periods) do |yielder, period| yielder << period end
15
+ end
16
+
17
+ delegate time_zone: :schedule
18
+
19
+ def timeline
20
+ Timeline.new(self)
21
+ end
22
+
23
+ private
24
+
25
+ def periods
26
+ weeks.lazy.flat_map { |week|
27
+ business_periods(week)
28
+ }.select { |business_period|
29
+ relevant?(business_period)
30
+ }.map { |business_period|
31
+ business_period & boundary
32
+ }.reject { |business_period|
33
+ schedule.holidays.any? { |holiday|
34
+ holiday.contains?(business_period.start_time)
35
+ }
36
+ }
37
+ end
38
+
39
+ def business_periods(week)
40
+ intervals.lazy.map { |interval| interval.to_time_segment(week) }
41
+ end
42
+
43
+ def intervals
44
+ schedule.intervals
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ module Biz
2
+ class Periods
3
+ class After < Abstract
4
+
5
+ private
6
+
7
+ def weeks
8
+ Range.new(
9
+ Week.since_epoch(Time.new(time_zone).local(origin)),
10
+ Week.since_epoch(Time::HEAT_DEATH)
11
+ )
12
+ end
13
+
14
+ def relevant?(period)
15
+ origin < period.end_time
16
+ end
17
+
18
+ def boundary
19
+ TimeSegment.after(origin)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Biz
2
+ class Periods
3
+ class Before < Abstract
4
+
5
+ private
6
+
7
+ def weeks
8
+ Week.since_epoch(Time.new(time_zone).local(origin)).downto(
9
+ Week.since_epoch(Time::BIG_BANG)
10
+ )
11
+ end
12
+
13
+ def relevant?(period)
14
+ origin > period.start_time
15
+ end
16
+
17
+ def boundary
18
+ TimeSegment.before(origin)
19
+ end
20
+
21
+ def intervals
22
+ super.reverse
23
+ end
24
+
25
+ end
26
+ end
27
+ end