pacing 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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")
|