pacing 1.0.0 → 2.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.
- checksums.yaml +4 -4
- data/README.md +25 -4
- data/lib/pacing/error.rb +75 -0
- data/lib/pacing/normalizer.rb +210 -0
- data/lib/pacing/pacer.rb +56 -161
- data/lib/pacing/version.rb +1 -1
- data/lib/pacing.rb +2 -0
- data/pacing.gemspec +1 -1
- data/spec/pacing_spec.rb +416 -136
- metadata +5 -3
data/lib/pacing/pacer.rb
CHANGED
@@ -2,7 +2,6 @@ require 'date'
|
|
2
2
|
require 'holidays'
|
3
3
|
|
4
4
|
module Pacing
|
5
|
-
# two modes(strict: use start dates strictly in calculating pacing)
|
6
5
|
class Pacer
|
7
6
|
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
8
7
|
attr_reader :school_plan, :date, :non_business_days, :state, :mode, :interval, :summer_holidays
|
@@ -14,47 +13,45 @@ module Pacing
|
|
14
13
|
@state = state
|
15
14
|
@mode = [:strict, :liberal].include?(mode) ? mode : :liberal
|
16
15
|
|
17
|
-
|
18
|
-
raise TypeError.new("School plan must be a hash") if @school_plan.class != Hash
|
19
|
-
|
20
|
-
raise ArgumentError.new('You must pass in a date') if @date.nil?
|
21
|
-
raise TypeError.new("The date should be formatted as a string in the format mm-dd-yyyy") if @date.class != String || !/(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])-(19|20)\d\d/.match?(@date)
|
22
|
-
raise ArgumentError.new('Date must be within the interval range of the school plan') if !date_within_range
|
23
|
-
|
24
|
-
@non_business_days.each do |non_business_day|
|
25
|
-
raise TypeError.new('"Non business days" dates should be formatted as a string in the format mm-dd-yyyy') if non_business_day.class != String || !/(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])-(19|20)\d\d/.match?(non_business_day)
|
26
|
-
end
|
27
|
-
|
28
|
-
@school_plan[:school_plan_services].each do |school_plan_service|
|
29
|
-
raise TypeError.new("School plan type must be a string and cannot be nil") if school_plan_service[:school_plan_type].class != String || school_plan_service[:school_plan_type].nil?
|
30
|
-
|
31
|
-
raise ArgumentError.new("School plan services start and end dates can not be nil") if school_plan_service[:start_date].nil? || school_plan_service[:end_date].nil?
|
32
|
-
|
33
|
-
raise TypeError.new("School plan services start and end dates should be formatted as a string in the format mm-dd-yyyy") if school_plan_service[:start_date].class != String || !/(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])-(19|20)\d\d/.match?(school_plan_service[:start_date])
|
34
|
-
|
35
|
-
raise TypeError.new("School plan services start and end dates should be formatted as a string in the format mm-dd-yyyy") if school_plan_service[:end_date].class != String || !/(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])-(19|20)\d\d/.match?(school_plan_service[:end_date])
|
16
|
+
Pacing::Error.new(school_plan: school_plan, date: date, non_business_days: non_business_days, state: state, mode: mode, summer_holidays: summer_holidays)
|
36
17
|
|
37
|
-
|
38
|
-
|
39
|
-
raise TypeError.new("Frequency must be an integer and cannot be nil") if school_plan_service[:frequency].class != Integer || school_plan_service[:frequency].nil?
|
40
|
-
|
41
|
-
raise TypeError.new("Interval must be a string and cannot be nil") if school_plan_service[:interval].class != String || school_plan_service[:interval].nil?
|
18
|
+
@summer_holidays = summer_holidays.empty? ? parse_summer_holiday_dates : [parse_date(summer_holidays[0]), parse_date(summer_holidays[1])]
|
19
|
+
end
|
42
20
|
|
43
|
-
|
21
|
+
def interval
|
22
|
+
# filter out services that haven't started or whose time is passed
|
23
|
+
services = @school_plan[:school_plan_services].filter do |school_plan_service|
|
24
|
+
within = true
|
25
|
+
if !(parse_date(school_plan_service[:start_date]) <= parse_date(@date) && parse_date(@date) <= parse_date(school_plan_service[:end_date]))
|
26
|
+
within = false
|
27
|
+
end
|
44
28
|
|
45
|
-
|
29
|
+
within
|
30
|
+
end
|
46
31
|
|
47
|
-
|
32
|
+
services = services.map do |service|
|
33
|
+
if ["pragmatic language", "speech and language", "language", "speech", "language therapy", "speech therapy", "speech and language therapy", "speech language therapy"].include?(service[:type_of_service].downcase)
|
34
|
+
discipline_name = "Speech Therapy"
|
35
|
+
elsif ["occupation therapy", "occupational therapy"].include?(service[:type_of_service].downcase)
|
36
|
+
discipline_name = "Occupational Therapy"
|
37
|
+
elsif ["physical therapy"].include?(service[:type_of_service].downcase)
|
38
|
+
discipline_name = "Physical Therapy"
|
39
|
+
elsif ["feeding therapy"].include?(service[:type_of_service].downcase)
|
40
|
+
discipline_name = "Feeding Therapy"
|
41
|
+
end
|
48
42
|
|
49
|
-
|
43
|
+
discipline = {}
|
44
|
+
discipline[:discipline] = discipline_name
|
45
|
+
discipline[:reset_date] = reset_date(start_date: service[:start_date], interval: service[:interval])
|
46
|
+
discipline[:start_date] = start_of_treatment_date(parse_date(service[:start_date]), service[:interval]).strftime("%m-%d-%Y")
|
47
|
+
discipline
|
50
48
|
end
|
51
|
-
|
52
|
-
@summer_holidays = summer_holidays.empty? ? parse_summer_holiday_dates : [parse_date(summer_holidays[0]), parse_date(summer_holidays[1])]
|
53
49
|
end
|
54
50
|
|
55
51
|
def calculate
|
56
52
|
# filter out services that haven't started or whose time is passed
|
57
|
-
|
53
|
+
school_plan_services = Pacing::Normalizer.new(@school_plan[:school_plan_services], @date).normalize
|
54
|
+
services = school_plan_services[:school_plan_services].filter do |school_plan_service|
|
58
55
|
within = true
|
59
56
|
if !(parse_date(school_plan_service[:start_date]) <= parse_date(@date) && parse_date(@date) <= parse_date(school_plan_service[:end_date]))
|
60
57
|
within = false
|
@@ -84,12 +81,17 @@ module Pacing
|
|
84
81
|
|
85
82
|
discipline[:expected_visits_at_date] = expected
|
86
83
|
|
87
|
-
discipline[:
|
84
|
+
discipline[:discipline] = service[:type_of_service]
|
85
|
+
|
86
|
+
discipline[:pace_indicator] = pace_indicator(discipline[:pace])
|
87
|
+
discipline[:pace_suggestion] = readable_suggestion(rate: discipline[:suggested_rate])
|
88
|
+
|
89
|
+
discipline.delete(:suggested_rate)
|
88
90
|
|
89
91
|
discipline
|
90
92
|
end
|
91
93
|
|
92
|
-
|
94
|
+
services
|
93
95
|
end
|
94
96
|
|
95
97
|
# get a spreadout of visit dates over an interval by using simple proportion.
|
@@ -117,11 +119,11 @@ module Pacing
|
|
117
119
|
|
118
120
|
def interval_days(interval)
|
119
121
|
case interval
|
120
|
-
when "monthly"
|
122
|
+
when "monthly", "per month"
|
121
123
|
return COMMON_YEAR_DAYS_IN_MONTH[(parse_date(@date)).month]
|
122
|
-
when "weekly"
|
124
|
+
when "weekly", "per week"
|
123
125
|
return 6
|
124
|
-
when "yearly"
|
126
|
+
when "yearly", "per year"
|
125
127
|
return parse_date(@date).leap? ? 366 : 365
|
126
128
|
end
|
127
129
|
end
|
@@ -179,11 +181,11 @@ module Pacing
|
|
179
181
|
# scoped to the interval
|
180
182
|
def reset_date(start_date:, interval:)
|
181
183
|
case interval
|
182
|
-
when "monthly"
|
184
|
+
when "monthly", "per month"
|
183
185
|
return reset_date_monthly(start_date, interval)
|
184
|
-
when "weekly"
|
186
|
+
when "weekly", "per week"
|
185
187
|
return reset_date_weekly(start_date, interval)
|
186
|
-
when "yearly"
|
188
|
+
when "yearly", "per year"
|
187
189
|
return reset_date_yearly(start_date)
|
188
190
|
end
|
189
191
|
end
|
@@ -191,11 +193,11 @@ module Pacing
|
|
191
193
|
# scoped to the interval
|
192
194
|
def start_of_treatment_date(start_date, interval="monthly")
|
193
195
|
case interval
|
194
|
-
when "monthly"
|
196
|
+
when "monthly", "per month"
|
195
197
|
return start_of_treatment_date_monthly(start_date)
|
196
|
-
when "weekly"
|
198
|
+
when "weekly", "per week"
|
197
199
|
return start_of_treatment_date_weekly(start_date)
|
198
|
-
when "yearly"
|
200
|
+
when "yearly", "per year"
|
199
201
|
return start_of_treatment_date_yearly(start_date)
|
200
202
|
end
|
201
203
|
end
|
@@ -215,10 +217,11 @@ module Pacing
|
|
215
217
|
|
216
218
|
# get actual date of the first day of the week where date falls
|
217
219
|
def week_start(date, offset_from_sunday=0)
|
220
|
+
offset_from_sunday = @mode == :liberal ? 1 : 0
|
218
221
|
return date if date.monday?
|
219
222
|
date - ((date.wday - offset_from_sunday) % 7)
|
220
223
|
end
|
221
|
-
|
224
|
+
|
222
225
|
# reset date for the yearly interval
|
223
226
|
def reset_date_yearly(start_date)
|
224
227
|
(parse_date(@date).leap? ? parse_date(start_date) + 366 : parse_date(start_date) + 365).strftime("%m-%d-%Y")
|
@@ -226,7 +229,9 @@ module Pacing
|
|
226
229
|
|
227
230
|
# reset date for the monthly interval
|
228
231
|
def reset_date_monthly(start_date, interval)
|
229
|
-
|
232
|
+
month = (parse_date(@date)).month
|
233
|
+
|
234
|
+
(start_of_treatment_date(parse_date(start_date), interval) + COMMON_YEAR_DAYS_IN_MONTH[month]).strftime("%m-%d-%Y")
|
230
235
|
end
|
231
236
|
|
232
237
|
# reset date for the weekly interval
|
@@ -245,7 +250,7 @@ module Pacing
|
|
245
250
|
# start_date
|
246
251
|
end
|
247
252
|
|
248
|
-
# start of treatment for the
|
253
|
+
# start of treatment for the monthly interval
|
249
254
|
def start_of_treatment_date_monthly(start_date)
|
250
255
|
if @mode == :strict
|
251
256
|
return parse_date("#{parse_date(@date).month}-#{start_date.day}-#{parse_date(@date).year}")
|
@@ -260,12 +265,14 @@ module Pacing
|
|
260
265
|
|
261
266
|
# start of treatment for the weekly interval
|
262
267
|
def start_of_treatment_date_weekly(start_date)
|
268
|
+
# TODO: Update with assumption that Monday is start of week
|
269
|
+
# Future TODO: allow user to pass in configuration for start of week
|
263
270
|
parsed_date = parse_date(@date)
|
264
271
|
week_start_date = week_start(parsed_date)
|
265
272
|
weekly_date = week_start_date
|
266
273
|
|
267
274
|
if week_start_date != parsed_date && @mode == :strict
|
268
|
-
weekly_date = week_start_date + start_date.wday #unless start_date.wday == 1
|
275
|
+
weekly_date = week_start_date + start_date.wday # unless start_date.wday == 1
|
269
276
|
weekly_date = parsed_date < weekly_date ? weekly_date - 7 : weekly_date
|
270
277
|
end
|
271
278
|
|
@@ -284,114 +291,8 @@ module Pacing
|
|
284
291
|
rescue => exception
|
285
292
|
valid_range_or_exceptions = true
|
286
293
|
end
|
287
|
-
|
288
|
-
valid_range_or_exceptions
|
289
|
-
end
|
290
294
|
|
291
|
-
|
292
|
-
discipline = {
|
293
|
-
:discipline => "Speech Therapy",
|
294
|
-
:remaining_visits => 0,
|
295
|
-
:used_visits => 0,
|
296
|
-
:pace => 0,
|
297
|
-
:pace_indicator => "🐢",
|
298
|
-
:pace_suggestion => "once a day",
|
299
|
-
:suggested_rate => 0,
|
300
|
-
:expected_visits_at_date => 0,
|
301
|
-
:reset_date => nil } # some arbitrarity date in the past
|
302
|
-
|
303
|
-
discipline_services = services.filter do |service|
|
304
|
-
["Language Therapy", "Speech Therapy", "Speech and Language Therapy", "Speech Language Therapy"].include? service[:type_of_service]
|
305
|
-
end
|
306
|
-
|
307
|
-
return {} if discipline_services.empty?
|
308
|
-
|
309
|
-
discipline_data(discipline_services, discipline)
|
310
|
-
end
|
311
|
-
|
312
|
-
def occupational_discipline(services)
|
313
|
-
discipline = {
|
314
|
-
:discipline=>"Occupational Therapy",
|
315
|
-
:remaining_visits=>0,
|
316
|
-
:used_visits=>0,
|
317
|
-
:pace=>0,
|
318
|
-
:pace_indicator=>"🐢",
|
319
|
-
:pace_suggestion=>"once a day",
|
320
|
-
:suggested_rate => 0,
|
321
|
-
:expected_visits_at_date=>0,
|
322
|
-
:reset_date=> nil } # some arbitrarity date in the past
|
323
|
-
|
324
|
-
discipline_services = services.filter do |service|
|
325
|
-
["occupation therapy", "occupational therapy"].include? (service[:type_of_service].downcase)
|
326
|
-
end
|
327
|
-
|
328
|
-
return {} if discipline_services.empty?
|
329
|
-
|
330
|
-
discipline_data(discipline_services, discipline)
|
331
|
-
end
|
332
|
-
|
333
|
-
def physical_discipline(services)
|
334
|
-
discipline = {
|
335
|
-
:discipline=>"Physical Therapy",
|
336
|
-
:remaining_visits=>0,
|
337
|
-
:used_visits=>0,
|
338
|
-
:pace=>0,
|
339
|
-
:pace_indicator=>"🐢",
|
340
|
-
:pace_suggestion=>"once a day",
|
341
|
-
:suggested_rate => 0,
|
342
|
-
:expected_visits_at_date=>0,
|
343
|
-
:reset_date=> nil } # some arbitrarity date in the past
|
344
|
-
|
345
|
-
discipline_services = services.filter do |service|
|
346
|
-
["Physical Therapy"].include? service[:type_of_service]
|
347
|
-
end
|
348
|
-
|
349
|
-
return {} if discipline_services.empty?
|
350
|
-
|
351
|
-
discipline_data(discipline_services, discipline)
|
352
|
-
end
|
353
|
-
|
354
|
-
def feeding_discipline(services)
|
355
|
-
discipline = {
|
356
|
-
:discipline=>"Feeding Therapy",
|
357
|
-
:remaining_visits=>0,
|
358
|
-
:used_visits=>0,
|
359
|
-
:pace=>0,
|
360
|
-
:pace_indicator=>"🐢",
|
361
|
-
:pace_suggestion=>"once a day",
|
362
|
-
:suggested_rate => 0,
|
363
|
-
:expected_visits_at_date=>0,
|
364
|
-
:reset_date=> nil } # some arbitrarity date in the past
|
365
|
-
|
366
|
-
discipline_services = services.filter do |service|
|
367
|
-
["Feeding Therapy"].include? service[:type_of_service]
|
368
|
-
end
|
369
|
-
|
370
|
-
return {} if discipline_services.empty?
|
371
|
-
|
372
|
-
discipline_data(discipline_services, discipline)
|
373
|
-
end
|
374
|
-
|
375
|
-
def discipline_data(services, discipline)
|
376
|
-
services.each do |service|
|
377
|
-
discipline[:pace] = discipline[:pace] ? discipline[:pace].to_i + service[:pace].to_i : service[:pace]
|
378
|
-
|
379
|
-
discipline[:remaining_visits] = discipline[:remaining_visits] ? discipline[:remaining_visits].to_i + service[:remaining_visits].to_i : service[:remaining_visits]
|
380
|
-
|
381
|
-
discipline[:used_visits] = discipline[:used_visits] ? discipline[:used_visits].to_i + service[:used_visits].to_i : service[:used_visits]
|
382
|
-
|
383
|
-
discipline[:expected_visits_at_date] = discipline[:expected_visits_at_date] ? discipline[:expected_visits_at_date].to_i + service[:expected_visits_at_date].to_i : service[:expected_visits_at_date]
|
384
|
-
|
385
|
-
discipline[:suggested_rate] = discipline[:suggested_rate] ? discipline[:suggested_rate].to_f + service[:suggested_rate].to_f : service[:suggested_rate].to_f
|
386
|
-
|
387
|
-
discipline[:reset_date] = (!discipline[:reset_date].nil? && parse_date(service[:reset_date]) < parse_date(discipline[:reset_date])) ? discipline[:reset_date] : service[:reset_date]
|
388
|
-
end
|
389
|
-
|
390
|
-
discipline[:pace_indicator] = pace_indicator(discipline[:pace])
|
391
|
-
discipline[:pace_suggestion] = readable_suggestion(rate: discipline[:suggested_rate])
|
392
|
-
|
393
|
-
discipline.delete(:suggested_rate)
|
394
|
-
discipline
|
295
|
+
valid_range_or_exceptions
|
395
296
|
end
|
396
297
|
|
397
298
|
def readable_suggestion(rate:)
|
@@ -429,11 +330,6 @@ module Pacing
|
|
429
330
|
days_left
|
430
331
|
end
|
431
332
|
|
432
|
-
def disciplines_cleaner(disciplines)
|
433
|
-
# use the fake arbitrary reset date to remove unrequired disciplines
|
434
|
-
disciplines.filter { |discipline| !discipline.empty? }
|
435
|
-
end
|
436
|
-
|
437
333
|
def parse_summer_holiday_dates
|
438
334
|
holidays_start = parse_date("05-13-#{parse_date(@date).year}")
|
439
335
|
holidays_start += 1 until holidays_start.wday == 5
|
@@ -442,7 +338,6 @@ module Pacing
|
|
442
338
|
holidays_start += 1 until holidays_start.wday == 1
|
443
339
|
|
444
340
|
[holidays_start, holidays_end]
|
445
|
-
end
|
341
|
+
end
|
446
342
|
end
|
447
343
|
end
|
448
|
-
|
data/lib/pacing/version.rb
CHANGED
data/lib/pacing.rb
CHANGED
data/pacing.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = 'pacing'
|
7
7
|
s.version = Pacing::VERSION
|
8
8
|
s.summary = "Pacing is a tool that enables therapists to better manage and track their caseload."
|
9
|
-
s.description = "Pacing is built for cases where there are therapy frequency limitations that need to be adhered to. For example, in the case of an [IEP (Individualized Education Program)](https://ambiki.com/glossary-concepts/iep), 504 plan, or a
|
9
|
+
s.description = "Pacing is built for cases where there are therapy frequency limitations that need to be adhered to. For example, in the case of an [IEP (Individualized Education Program)](https://ambiki.com/glossary-concepts/iep), 504 plan, or a Service plan. This gem helps to calculate remaining visits as well as a therapist's current pace to meet visit mandates."
|
10
10
|
s.authors = ["Kevin S. Dias", "Samuel Okoth", "Lukong I. Nsoseka"]
|
11
11
|
s.email = 'info@ambiki.com'
|
12
12
|
s.files = `git ls-files -z`.split("\x0")
|