timeboss 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +5 -0
- data/.replit +2 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +233 -0
- data/Rakefile +5 -0
- data/bin/tbsh +15 -0
- data/lib/tasks/calendars.rake +22 -0
- data/lib/tasks/timeboss.rake +6 -0
- data/lib/timeboss.rb +6 -0
- data/lib/timeboss/calendar.rb +64 -0
- data/lib/timeboss/calendar/day.rb +48 -0
- data/lib/timeboss/calendar/half.rb +22 -0
- data/lib/timeboss/calendar/month.rb +22 -0
- data/lib/timeboss/calendar/parser.rb +53 -0
- data/lib/timeboss/calendar/period.rb +83 -0
- data/lib/timeboss/calendar/quarter.rb +22 -0
- data/lib/timeboss/calendar/support/formatter.rb +33 -0
- data/lib/timeboss/calendar/support/month_basis.rb +21 -0
- data/lib/timeboss/calendar/support/monthly_unit.rb +55 -0
- data/lib/timeboss/calendar/support/navigable.rb +72 -0
- data/lib/timeboss/calendar/support/shiftable.rb +241 -0
- data/lib/timeboss/calendar/support/translatable.rb +93 -0
- data/lib/timeboss/calendar/support/unit.rb +88 -0
- data/lib/timeboss/calendar/waypoints.rb +12 -0
- data/lib/timeboss/calendar/waypoints/absolute.rb +113 -0
- data/lib/timeboss/calendar/waypoints/relative.rb +267 -0
- data/lib/timeboss/calendar/week.rb +53 -0
- data/lib/timeboss/calendar/year.rb +18 -0
- data/lib/timeboss/calendars.rb +53 -0
- data/lib/timeboss/calendars/broadcast.rb +32 -0
- data/lib/timeboss/calendars/gregorian.rb +30 -0
- data/lib/timeboss/support/shellable.rb +17 -0
- data/lib/timeboss/version.rb +4 -0
- data/spec/calendar/day_spec.rb +60 -0
- data/spec/calendar/quarter_spec.rb +32 -0
- data/spec/calendar/support/monthly_unit_spec.rb +85 -0
- data/spec/calendar/support/unit_spec.rb +90 -0
- data/spec/calendar/week_spec.rb +80 -0
- data/spec/calendars/broadcast_spec.rb +796 -0
- data/spec/calendars/gregorian_spec.rb +684 -0
- data/spec/calendars_spec.rb +50 -0
- data/spec/spec_helper.rb +12 -0
- data/timeboss.gemspec +31 -0
- metadata +216 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'calendar'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
module Calendars
|
6
|
+
extend self
|
7
|
+
extend Enumerable
|
8
|
+
delegate :each, :length, to: :all
|
9
|
+
|
10
|
+
# Register a new calendar
|
11
|
+
# @return [Entry]
|
12
|
+
def register(name, klass)
|
13
|
+
Entry.new(name.to_sym, klass).tap do |entry|
|
14
|
+
(@entries ||= {})[name.to_sym] = entry
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Retrieve a list of all registered calendars.
|
19
|
+
# @return [Array<Entry>]
|
20
|
+
def all
|
21
|
+
return if @entries.nil?
|
22
|
+
@entries.values.sort_by { |e| e.name.to_s }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieve an instance of the specified named calendar.
|
26
|
+
# @param name [String, Symbol] the name of the calendar to retrieve.
|
27
|
+
# @return [Calendar]
|
28
|
+
def [](name)
|
29
|
+
return if @entries.nil?
|
30
|
+
@entries[name&.to_sym]&.calendar
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
Entry = Struct.new(:name, :klass) do
|
36
|
+
# @!method name
|
37
|
+
# Get the name of the calendar referenced in this entry.
|
38
|
+
# @return [Symbol]
|
39
|
+
|
40
|
+
# @!method klass
|
41
|
+
# The class implementing this calendar.
|
42
|
+
# @return [Class<Calendar>]
|
43
|
+
|
44
|
+
# Get an instance of the calendar referenced in this entry.
|
45
|
+
# @return [Calendar]
|
46
|
+
def calendar
|
47
|
+
@_calendar ||= klass.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Dir[File.expand_path('../calendars/*.rb', __FILE__)].each { |f| require f }
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../calendar'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
module Calendars
|
6
|
+
class Broadcast < Calendar
|
7
|
+
register!
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(basis: Basis)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
class Basis < Calendar::Support::MonthBasis
|
16
|
+
def start_date
|
17
|
+
@_start_date ||= begin
|
18
|
+
date = Date.civil(year, month, 1)
|
19
|
+
date - (date.wday + 6) % 7
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def end_date
|
24
|
+
@_end_date ||= begin
|
25
|
+
date = Date.civil(year, month, -1)
|
26
|
+
date - date.wday
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../calendar'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
module Calendars
|
6
|
+
class Gregorian < Calendar
|
7
|
+
register!
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(basis: Basis)
|
11
|
+
end
|
12
|
+
|
13
|
+
def supports_weeks?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
class Basis < Calendar::Support::MonthBasis
|
20
|
+
def start_date
|
21
|
+
@_start_date ||= Date.civil(year, month, 1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def end_date
|
25
|
+
@_end_date ||= Date.civil(year, month, -1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
module Support
|
3
|
+
module Shellable
|
4
|
+
def self.open(context)
|
5
|
+
context.extend(self).open_shell
|
6
|
+
end
|
7
|
+
|
8
|
+
def open_shell
|
9
|
+
require 'irb'
|
10
|
+
IRB.setup nil
|
11
|
+
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
|
12
|
+
require 'irb/ext/multi-irb'
|
13
|
+
IRB.irb nil, self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
describe Day do
|
4
|
+
let(:calendar) { instance_double(TimeBoss::Calendar) }
|
5
|
+
let(:start_date) { Date.parse('2019-09-30') }
|
6
|
+
let(:subject) { described_class.new(calendar, start_date) }
|
7
|
+
|
8
|
+
it 'knows its stuff' do
|
9
|
+
expect(subject.start_date).to eq start_date
|
10
|
+
expect(subject.end_date).to eq start_date
|
11
|
+
expect(subject.to_range).to eq start_date..start_date
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'knows its name' do
|
15
|
+
expect(subject.name).to eq start_date.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'knows its title' do
|
19
|
+
expect(subject.title).to eq 'September 30, 2019'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'can stringify itself' do
|
23
|
+
expect(subject.to_s).to eq subject.name
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#index' do
|
27
|
+
before(:each) { allow(subject).to receive(:year).and_return double(start_date: start_date - 3) }
|
28
|
+
|
29
|
+
it 'gets its index within the year' do
|
30
|
+
expect(subject.index).to eq 4
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#current?' do
|
35
|
+
it 'knows when it is' do
|
36
|
+
allow(Date).to receive(:today).and_return start_date
|
37
|
+
expect(subject).to be_current
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'knows when it is not' do
|
41
|
+
expect(subject).not_to be_current
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'navigation' do
|
46
|
+
it 'can get the previous date' do
|
47
|
+
result = subject.previous
|
48
|
+
expect(result).to be_a described_class
|
49
|
+
expect(result.start_date).to eq start_date - 1.day
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'can get the next date' do
|
53
|
+
result = subject.next
|
54
|
+
expect(result).to be_a described_class
|
55
|
+
expect(result.start_date).to eq start_date + 1.day
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
describe Quarter do
|
4
|
+
let(:calendar) { instance_double(TimeBoss::Calendar) }
|
5
|
+
let(:start_date) { Date.parse('2019-09-30') }
|
6
|
+
let(:end_date) { Date.parse('2019-12-29') }
|
7
|
+
let(:subject) { described_class.new(calendar, 2019, 4, start_date, end_date) }
|
8
|
+
|
9
|
+
it 'knows its stuff' do
|
10
|
+
expect(subject.name).to eq '2019Q4'
|
11
|
+
expect(subject.start_date).to eq start_date
|
12
|
+
expect(subject.end_date).to eq end_date
|
13
|
+
expect(subject.to_range).to eq start_date..end_date
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can stringify itself' do
|
17
|
+
expect(subject.to_s).to include('2019Q4', start_date.to_s, end_date.to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#current?' do
|
21
|
+
it 'knows when it is' do
|
22
|
+
allow(Date).to receive(:today).and_return start_date
|
23
|
+
expect(subject).to be_current
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'knows when it is not' do
|
27
|
+
expect(subject).not_to be_current
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
module Support
|
4
|
+
describe MonthlyUnit do
|
5
|
+
class MonthBasedChunk < described_class
|
6
|
+
NUM_MONTHS = 2
|
7
|
+
|
8
|
+
def name
|
9
|
+
"#{year_index}C#{index}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
let(:described_class) { MonthBasedChunk }
|
13
|
+
let(:calendar) { double }
|
14
|
+
let(:start_date) { Date.parse('2018-06-25') }
|
15
|
+
let(:end_date) { Date.parse('2018-08-26') }
|
16
|
+
let(:subject) { described_class.new(calendar, 2018, 4, start_date, end_date) }
|
17
|
+
|
18
|
+
it 'knows its stuff' do
|
19
|
+
expect(subject.start_date).to eq start_date
|
20
|
+
expect(subject.end_date).to eq end_date
|
21
|
+
expect(subject.to_range).to eq start_date..end_date
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'can stringify itself' do
|
25
|
+
expect(subject.to_s).to eq "2018C4: 2018-06-25 thru 2018-08-26"
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#weeks' do
|
29
|
+
let(:base) { double(start_date: Date.parse('2018-01-01'), end_date: Date.parse('2018-12-30')) }
|
30
|
+
before(:each) { allow(calendar).to receive(:year).with(2018).and_return base }
|
31
|
+
|
32
|
+
it 'can get the relevant weeks for the period' do
|
33
|
+
allow(calendar).to receive(:supports_weeks?).and_return true
|
34
|
+
result = subject.weeks
|
35
|
+
result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
|
36
|
+
expect(result.map { |w| w.start_date.to_s }).to eq [
|
37
|
+
'2018-06-25',
|
38
|
+
'2018-07-02',
|
39
|
+
'2018-07-09',
|
40
|
+
'2018-07-16',
|
41
|
+
'2018-07-23',
|
42
|
+
'2018-07-30',
|
43
|
+
'2018-08-06',
|
44
|
+
'2018-08-13',
|
45
|
+
'2018-08-20'
|
46
|
+
]
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'blows up when weeks are not supported' do
|
50
|
+
allow(calendar).to receive(:supports_weeks?).and_return false
|
51
|
+
expect { subject.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'navigation' do
|
56
|
+
let(:result) { double }
|
57
|
+
|
58
|
+
describe '#previous' do
|
59
|
+
it 'moves easily within itself' do
|
60
|
+
expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
|
61
|
+
expect(described_class.new(calendar, 48, 4, nil, nil).previous).to eq result
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'flips to the previous container' do
|
65
|
+
expect(calendar).to receive(:month_based_chunk).with(47, 6).and_return result
|
66
|
+
expect(described_class.new(calendar, 48, 1, nil, nil).previous).to eq result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#next' do
|
71
|
+
it 'moves easily within itself' do
|
72
|
+
expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
|
73
|
+
expect(described_class.new(calendar, 48, 2, nil, nil).next).to eq result
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'flips to the previous container' do
|
77
|
+
expect(calendar).to receive(:month_based_chunk).with(48, 1).and_return result
|
78
|
+
expect(described_class.new(calendar, 47, 6, nil, nil).next).to eq result
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
module Support
|
4
|
+
describe Unit do
|
5
|
+
class ChunkUnit < described_class
|
6
|
+
end
|
7
|
+
let(:described_class) { ChunkUnit }
|
8
|
+
let(:calendar) { double }
|
9
|
+
let(:start_date) { Date.parse('2018-06-25') }
|
10
|
+
let(:end_date) { Date.parse('2018-08-26') }
|
11
|
+
let(:subject) { described_class.new(calendar, start_date, end_date) }
|
12
|
+
|
13
|
+
it 'knows its stuff' do
|
14
|
+
expect(subject.start_date).to eq start_date
|
15
|
+
expect(subject.end_date).to eq end_date
|
16
|
+
expect(subject.to_range).to eq start_date..end_date
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#current?' do
|
20
|
+
it 'is not current right now' do
|
21
|
+
expect(subject).not_to be_current
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'is current when today falls in the middle' do
|
25
|
+
allow(Date).to receive(:today).and_return start_date + 3.days
|
26
|
+
expect(subject).to be_current
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'periods' do
|
31
|
+
before(:each) do
|
32
|
+
allow(calendar).to receive(:days_for).with(subject).and_return %w[D1 D2 D3 D4 D5 D6 D7 D8]
|
33
|
+
allow(calendar).to receive(:weeks_for).with(subject).and_return %w[W1 W2 W3 W4]
|
34
|
+
allow(calendar).to receive(:months_for).with(subject).and_return %w[M1 M2 M3]
|
35
|
+
allow(calendar).to receive(:quarters_for).with(subject).and_return %w[Q1 Q2]
|
36
|
+
allow(calendar).to receive(:halves_for).with(subject).and_return %w[H1]
|
37
|
+
allow(calendar).to receive(:years_for).with(subject).and_return %w[Y1]
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'knows about its days' do
|
41
|
+
expect(subject.days).to eq %w[D1 D2 D3 D4 D5 D6 D7 D8]
|
42
|
+
expect(subject.day).to be nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'knows about its weeks' do
|
46
|
+
expect(subject.weeks).to eq %w[W1 W2 W3 W4]
|
47
|
+
expect(subject.week).to be nil
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'knows about its months' do
|
51
|
+
expect(subject.months).to eq %w[M1 M2 M3]
|
52
|
+
expect(subject.month).to be nil
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'knows about its quarters' do
|
56
|
+
expect(subject.quarters).to eq %w[Q1 Q2]
|
57
|
+
expect(subject.quarter).to be nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'knows about its halves' do
|
61
|
+
expect(subject.halves).to eq %w[H1]
|
62
|
+
expect(subject.half).to eq 'H1'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'knows about its years' do
|
66
|
+
expect(subject.years).to eq %w[Y1]
|
67
|
+
expect(subject.year).to eq 'Y1'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'navigation' do
|
72
|
+
let(:result) { double }
|
73
|
+
|
74
|
+
describe '#offset' do
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'can increment' do
|
78
|
+
expect(subject).to receive(:offset).with(7).and_return result
|
79
|
+
expect(subject + 7).to eq result
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'can decrement' do
|
83
|
+
expect(subject).to receive(:offset).with(-23).and_return result
|
84
|
+
expect(subject - 23).to eq result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
describe Week do
|
4
|
+
let(:calendar) { instance_double(TimeBoss::Calendar, supports_weeks?: true) }
|
5
|
+
let(:start_date) { Date.parse('2048-04-06') }
|
6
|
+
let(:end_date) { Date.parse('2048-04-12') }
|
7
|
+
let(:subject) { described_class.new(calendar, start_date, end_date) }
|
8
|
+
|
9
|
+
it "doesn't even exist if its calendar doesn't support weeks" do
|
10
|
+
allow(calendar).to receive(:supports_weeks?).and_return false
|
11
|
+
expect { subject }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'knows its stuff' do
|
15
|
+
expect(subject.start_date).to eq start_date
|
16
|
+
expect(subject.end_date).to eq end_date
|
17
|
+
expect(subject.to_range).to eq start_date..end_date
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'knows its title' do
|
21
|
+
expect(subject.title).to eq "Week of April 6, 2048"
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#current?' do
|
25
|
+
it 'knows when it is' do
|
26
|
+
allow(Date).to receive(:today).and_return start_date
|
27
|
+
expect(subject).to be_current
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'knows when it is not' do
|
31
|
+
expect(subject).not_to be_current
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'navigation' do
|
36
|
+
let(:calendar) { TimeBoss::Calendars::Broadcast.new }
|
37
|
+
|
38
|
+
describe '#previous' do
|
39
|
+
it 'can back up simply' do
|
40
|
+
result = subject.previous
|
41
|
+
expect(result).to be_a described_class
|
42
|
+
expect(result.to_s).to eq "2048W14: 2048-03-30 thru 2048-04-05"
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'can wrap to the previous 52-week year' do
|
46
|
+
result = described_class.new(calendar, Date.parse('2021-12-27'), Date.parse('2022-01-02')).previous
|
47
|
+
expect(result).to be_a described_class
|
48
|
+
expect(result.to_s).to eq "2021W52: 2021-12-20 thru 2021-12-26"
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'can wrap to the previous 53-week year' do
|
52
|
+
result = described_class.new(calendar, Date.parse('2024-01-01'), Date.parse('2024-01-07')).previous
|
53
|
+
expect(result).to be_a described_class
|
54
|
+
expect(result.to_s).to eq "2023W53: 2023-12-25 thru 2023-12-31"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#next' do
|
59
|
+
it 'can move forward simply' do
|
60
|
+
result = subject.next
|
61
|
+
expect(result).to be_a described_class
|
62
|
+
expect(result.to_s).to eq "2048W16: 2048-04-13 thru 2048-04-19"
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'can wrap from week 52 to the next year' do
|
66
|
+
result = described_class.new(calendar, Date.parse('2021-12-20'), Date.parse('2021-12-26')).next
|
67
|
+
expect(result).to be_a described_class
|
68
|
+
expect(result.to_s).to eq "2022W1: 2021-12-27 thru 2022-01-02"
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'can wrap from week 53 to the next year' do
|
72
|
+
result = described_class.new(calendar, Date.parse('2023-12-25'), Date.parse('2023-12-31')).next
|
73
|
+
expect(result).to be_a described_class
|
74
|
+
expect(result.to_s).to eq "2024W1: 2024-01-01 thru 2024-01-07"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|