pacing 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
- raise ArgumentError.new("You must pass in at least one school plan") if @school_plan.nil?
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
- raise TypeError.new("Type of service must be a string and cannot be nil") if school_plan_service[:type_of_service].class != String || school_plan_service[:type_of_service].nil?
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
- raise TypeError.new("Time per session in minutes must be an integer and cannot be nil") if school_plan_service[:time_per_session_in_minutes].class != Integer || school_plan_service[:time_per_session_in_minutes].nil?
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
- raise TypeError.new("Completed visits for current interval must be an integer and cannot be nil") if school_plan_service[:completed_visits_for_current_interval].class != Integer || school_plan_service[:completed_visits_for_current_interval].nil?
29
+ within
30
+ end
46
31
 
47
- raise TypeError.new("Extra sessions allowable must be an integer and cannot be nil") if school_plan_service[:extra_sessions_allowable].class != Integer || school_plan_service[:extra_sessions_allowable].nil?
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
- raise TypeError.new("Interval for extra sessions allowable must be a string and cannot be nil") if school_plan_service[:interval_for_extra_sessions_allowable].class != String || school_plan_service[:interval_for_extra_sessions_allowable].nil?
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
- services = @school_plan[:school_plan_services].filter do |school_plan_service|
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[:type_of_service] = service[:type_of_service]
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
- disciplines_cleaner ([speech_discipline(services), occupational_discipline(services), physical_discipline(services), feeding_discipline(services)])
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
- (start_of_treatment_date(parse_date(start_date), interval) + COMMON_YEAR_DAYS_IN_MONTH[(parse_date(@date)).month]).strftime("%m-%d-%Y")
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 montly interval
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
- def speech_discipline(services)
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
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pacing
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/pacing.rb CHANGED
@@ -1,2 +1,4 @@
1
+ require "pacing/normalizer"
2
+ require "pacing/error"
1
3
  require "pacing/pacer"
2
4
  require "pacing/version"
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 Services plan. This gem helps to calculate remaining visits as well as a therapist's current pace to meet visit mandates."
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")