biz 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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