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.
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")