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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a71bff11bdcc21f74452c5217d348302f8e3680e3c2ed9d753ba1acd156f4d4a
|
4
|
+
data.tar.gz: b78da81e0108e399aba1b422f56ce5509948d2708e8b6c5a011a0f82d91086fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87c3abd50e088736eae74610469c86972c2ec09c0e8da94d3c7e27dd02e34d62aef25ddab2acf602d90d46e8aefd515d3b1d6102fac37dd813bcbd24ef71f507
|
7
|
+
data.tar.gz: 98a2d9a5b3fc8bd62630273ead673bce8a2c2ec3667dd706b3e3ab8efdbdd6db95df39501d8ac0a08170db4b40d6c66439fad08f6937a8c1b61bc97051377c7e
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Pacing
|
2
2
|
|
3
|
-
Pacing is a tool that enables therapists to better manage and track their caseload. It 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
|
3
|
+
Pacing is a tool that enables therapists to better manage and track their caseload. It 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.
|
4
4
|
|
5
5
|
- 🐇 Ahead of pace
|
6
6
|
- 😁 On pace
|
@@ -31,7 +31,7 @@ gem 'pacing'
|
|
31
31
|
school_plan = {
|
32
32
|
school_plan_services: [
|
33
33
|
{
|
34
|
-
school_plan_type: "IEP", # string ('IEP', '504 Plan', '
|
34
|
+
school_plan_type: "IEP", # string ('IEP', '504 Plan', 'Service Plan' )
|
35
35
|
start_date: "01-01-2022", # string (mm-dd-yyyy)
|
36
36
|
end_date: "01-01-2023", # string (mm-dd-yyyy)
|
37
37
|
type_of_service: "Language Therapy", # string ('Language Therapy', 'Speech Therapy', 'Occupation Therapy', 'Physical Therapy', 'Feeding Therapy', 'Speech and Language Therapy')
|
@@ -43,7 +43,7 @@ school_plan = {
|
|
43
43
|
interval_for_extra_sessions_allowable: "monthly", # string ('weekly', 'monthly', 'yearly')
|
44
44
|
},
|
45
45
|
{
|
46
|
-
school_plan_type: "IEP", # string ('IEP', '504 Plan', '
|
46
|
+
school_plan_type: "IEP", # string ('IEP', '504 Plan', 'Service Plan' )
|
47
47
|
start_date: "01-01-2022", # string (mm-dd-yyyy)
|
48
48
|
end_date: "01-01-2023", # string (mm-dd-yyyy)
|
49
49
|
type_of_service: "Physical Therapy", # string ('Language Therapy', 'Speech Therapy', 'Occupation Therapy', 'Physical Therapy', 'Feeding Therapy', 'Speech and Language Therapy')
|
@@ -116,8 +116,29 @@ paced.calculate
|
|
116
116
|
}
|
117
117
|
]
|
118
118
|
=end
|
119
|
+
|
120
|
+
paced.interval # Return current interval start and end dates
|
121
|
+
|
122
|
+
# Below is the result you will get
|
123
|
+
=begin
|
124
|
+
=> [
|
125
|
+
{
|
126
|
+
discipline: 'Speech Therapy',
|
127
|
+
start_date: '04-01-2022',
|
128
|
+
reset_date: '05-01-2022'
|
129
|
+
},
|
130
|
+
{
|
131
|
+
discipline: 'Physical Therapy',
|
132
|
+
start_date: '04-01-2022',
|
133
|
+
reset_date: '05-01-2022'
|
134
|
+
}
|
135
|
+
]
|
136
|
+
# =>
|
137
|
+
=end
|
119
138
|
```
|
120
139
|
|
140
|
+
Interval `start_date` and `end_date`'s are different from start and end dates for the service. Here they define the start and end dates under evaluation in a specific interval, be it `month`, `week`, or `year`. They represent when the bounds of an interval.
|
141
|
+
|
121
142
|
It is important to note that the `pace` is hugely influenced by the `summer_holidays` period and the `mode` in which it is calculated.
|
122
143
|
|
123
144
|
## Data Types
|
@@ -155,7 +176,7 @@ The following list shows the various variables and what they consist of:
|
|
155
176
|
|
156
177
|
- **IEP (Individualized Education Program)**: Individualized Education Programs (IEPs) are required by law for every student who receives special education services and are developed on an annual basis. The IEP is an educational document that the school generates. The therapist is bound to the frequency on the IEP, and the insurance companies will not pay for anything above or beyond what is on the IEP. It is a blueprint for a student’s special education experience in a public school. The plan must ensure that the child receives a free appropriate public education, (FAPE).
|
157
178
|
- **504 Plan**: 504 plans are formal plans that schools develop to give kids with disabilities the support they need. That covers any condition that limits daily activities in a major way. These plans prevent discrimination and they protect the rights of kids with disabilities in school. They are covered under Section 504 of the Rehabilitation Act, a civil rights law.
|
158
|
-
- **
|
179
|
+
- **Service Plan**: A plan paid for by the local school district for students with disabilities who attend private schools. A Service plan does not have to ensure a child is provided with FAPE (free appropriate public education). A Service plan spells out the special education and related services the LEA will make available to a child. These services are provided at no cost to parents. But the student may not be able to receive these services at the private school. Instead, the LEA can require him to go to a public school for services like speech therapy sessions. [§34 CFR 300.130 through §300.144 of IDEA](https://sites.ed.gov/idea/files/CWD_Enrolled_by_Their_Parents_in_Private_Schools_11-16-06.pdf) is a specific section that describes how services are provided to kids in private school.
|
159
180
|
|
160
181
|
## Testing
|
161
182
|
|
data/lib/pacing/error.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'holidays'
|
3
|
+
|
4
|
+
module Pacing
|
5
|
+
class Error
|
6
|
+
attr_reader :school_plan, :date, :non_business_days, :state, :mode, :interval, :summer_holidays
|
7
|
+
|
8
|
+
def initialize(school_plan:, date:, non_business_days:, state: :us_tn, mode: :liberal, summer_holidays: [])
|
9
|
+
@school_plan = school_plan
|
10
|
+
@non_business_days = non_business_days
|
11
|
+
@date = date
|
12
|
+
@state = state
|
13
|
+
@mode = [:strict, :liberal].include?(mode) ? mode : :liberal
|
14
|
+
|
15
|
+
raise ArgumentError.new("You must pass in at least one school plan") if school_plan.nil?
|
16
|
+
raise TypeError.new("School plan must be a hash") if school_plan.class != Hash
|
17
|
+
|
18
|
+
raise ArgumentError.new('You must pass in a date') if date.nil?
|
19
|
+
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)
|
20
|
+
raise ArgumentError.new('Date must be within the interval range of the school plan') if !date_within_range
|
21
|
+
|
22
|
+
non_business_days.each do |non_business_day|
|
23
|
+
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)
|
24
|
+
end
|
25
|
+
|
26
|
+
school_plan[:school_plan_services].each do |school_plan_service|
|
27
|
+
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?
|
28
|
+
|
29
|
+
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?
|
30
|
+
|
31
|
+
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])
|
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[: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])
|
34
|
+
|
35
|
+
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?
|
36
|
+
|
37
|
+
raise TypeError.new("Frequency must be an integer and cannot be nil") if school_plan_service[:frequency].class != Integer || school_plan_service[:frequency].nil?
|
38
|
+
|
39
|
+
raise TypeError.new("Interval must be a string and cannot be nil") if school_plan_service[:interval].class != String || school_plan_service[:interval].nil?
|
40
|
+
|
41
|
+
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?
|
42
|
+
|
43
|
+
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?
|
44
|
+
|
45
|
+
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?
|
46
|
+
|
47
|
+
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?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def date_within_range
|
52
|
+
valid_range_or_exceptions = false
|
53
|
+
|
54
|
+
begin
|
55
|
+
@school_plan[:school_plan_services].each do |school_plan_service|
|
56
|
+
if (parse_date(school_plan_service[:start_date]) < parse_date(@date) && parse_date(@date) < parse_date(school_plan_service[:end_date]))
|
57
|
+
valid_range_or_exceptions = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
rescue => exception
|
61
|
+
valid_range_or_exceptions = true
|
62
|
+
end
|
63
|
+
|
64
|
+
valid_range_or_exceptions
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_date(date)
|
68
|
+
begin
|
69
|
+
Date.strptime(date, '%m-%d-%Y')
|
70
|
+
rescue => exception
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'holidays'
|
3
|
+
|
4
|
+
module Pacing
|
5
|
+
class Normalizer
|
6
|
+
|
7
|
+
attr_accessor :services, :date
|
8
|
+
|
9
|
+
def initialize(services, date)
|
10
|
+
@date = date
|
11
|
+
@services = active_services(services)
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize
|
15
|
+
{ school_plan_services: disciplines_cleaner([speech_discipline, occupational_discipline, physical_discipline, feeding_discipline]) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def speech_discipline
|
19
|
+
discipline = {
|
20
|
+
:school_plan_type => 'IEP',
|
21
|
+
:start_date => "01-01-2100", # some arbitrary start date
|
22
|
+
:end_date => "01-01-2000", # some arbitrary end date
|
23
|
+
:type_of_service => 'Speech Therapy',
|
24
|
+
:frequency => 0,
|
25
|
+
:interval => '',
|
26
|
+
:time_per_session_in_minutes => 0,
|
27
|
+
:completed_visits_for_current_interval => 0,
|
28
|
+
:extra_sessions_allowable => 0,
|
29
|
+
:interval_for_extra_sessions_allowable => ''
|
30
|
+
}
|
31
|
+
|
32
|
+
discipline_services = services.filter do |service|
|
33
|
+
["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
|
+
end
|
35
|
+
|
36
|
+
return {} if discipline_services.empty?
|
37
|
+
|
38
|
+
discipline_services = normalize_to_monthly_frequency(discipline_services)
|
39
|
+
|
40
|
+
discipline_data(discipline_services, discipline)
|
41
|
+
end
|
42
|
+
|
43
|
+
def occupational_discipline
|
44
|
+
discipline = {
|
45
|
+
:school_plan_type => 'IEP',
|
46
|
+
:start_date => "01-01-2100", # some arbitrary start date
|
47
|
+
:end_date => "01-01-2000", # some arbitrary end date
|
48
|
+
:type_of_service => 'Occupational Therapy',
|
49
|
+
:frequency => 0,
|
50
|
+
:interval => '',
|
51
|
+
:time_per_session_in_minutes => 0,
|
52
|
+
:completed_visits_for_current_interval => 0,
|
53
|
+
:extra_sessions_allowable => 0,
|
54
|
+
:interval_for_extra_sessions_allowable => ''
|
55
|
+
}
|
56
|
+
|
57
|
+
discipline_services = services.filter do |service|
|
58
|
+
["occupation therapy", "occupational therapy", "occupation"].include?(service[:type_of_service].downcase)
|
59
|
+
end
|
60
|
+
|
61
|
+
return {} if discipline_services.empty?
|
62
|
+
|
63
|
+
discipline_services = normalize_to_monthly_frequency(discipline_services)
|
64
|
+
|
65
|
+
discipline_data(discipline_services, discipline)
|
66
|
+
end
|
67
|
+
|
68
|
+
def physical_discipline
|
69
|
+
discipline = {
|
70
|
+
:school_plan_type => 'IEP',
|
71
|
+
:start_date => "01-01-2100", # some arbitrary start date
|
72
|
+
:end_date => "01-01-2000", # some arbitrary end date
|
73
|
+
:type_of_service => 'Physical Therapy',
|
74
|
+
:frequency => 0,
|
75
|
+
:interval => '',
|
76
|
+
:time_per_session_in_minutes => 0,
|
77
|
+
:completed_visits_for_current_interval => 0,
|
78
|
+
:extra_sessions_allowable => 0,
|
79
|
+
:interval_for_extra_sessions_allowable => ''
|
80
|
+
}
|
81
|
+
|
82
|
+
discipline_services = services.filter do |service|
|
83
|
+
["physical therapy", "physical"].include?(service[:type_of_service].downcase)
|
84
|
+
end
|
85
|
+
|
86
|
+
return {} if discipline_services.empty?
|
87
|
+
|
88
|
+
discipline_services = normalize_to_monthly_frequency(discipline_services)
|
89
|
+
|
90
|
+
discipline_data(discipline_services, discipline)
|
91
|
+
end
|
92
|
+
|
93
|
+
def feeding_discipline
|
94
|
+
discipline = {
|
95
|
+
:school_plan_type => 'IEP',
|
96
|
+
:start_date => "01-01-2100", # some arbitrary start date
|
97
|
+
:end_date => "01-01-2000", # some arbitrary end date
|
98
|
+
:type_of_service => 'Feeding Therapy',
|
99
|
+
:frequency => 0,
|
100
|
+
:interval => '',
|
101
|
+
:time_per_session_in_minutes => 0,
|
102
|
+
:completed_visits_for_current_interval => 0,
|
103
|
+
:extra_sessions_allowable => 0,
|
104
|
+
:interval_for_extra_sessions_allowable => ''
|
105
|
+
}
|
106
|
+
|
107
|
+
discipline_services = services.filter do |service|
|
108
|
+
["feeding therapy", "feeding"].include?(service[:type_of_service].downcase)
|
109
|
+
end
|
110
|
+
|
111
|
+
return {} if discipline_services.empty?
|
112
|
+
|
113
|
+
discipline_services = normalize_to_monthly_frequency(discipline_services)
|
114
|
+
|
115
|
+
discipline_data(discipline_services, discipline)
|
116
|
+
end
|
117
|
+
|
118
|
+
def discipline_data(services, discipline)
|
119
|
+
services.each do |service|
|
120
|
+
discipline[:start_date] = parse_date(service[:start_date]) < parse_date(discipline[:start_date]) ? service[:start_date] : discipline[:start_date]
|
121
|
+
|
122
|
+
discipline[:end_date] = parse_date(service[:end_date]) > parse_date(discipline[:end_date]) ? service[:end_date] : discipline[:end_date]
|
123
|
+
|
124
|
+
discipline[:frequency] += service[:frequency].to_i
|
125
|
+
|
126
|
+
discipline[:completed_visits_for_current_interval] = service[:completed_visits_for_current_interval] if service[:completed_visits_for_current_interval] > discipline[:completed_visits_for_current_interval]
|
127
|
+
|
128
|
+
discipline[:time_per_session_in_minutes] = service[:time_per_session_in_minutes] > discipline[:time_per_session_in_minutes] ? service[:time_per_session_in_minutes] : discipline[:time_per_session_in_minutes]
|
129
|
+
|
130
|
+
discipline[:interval] = service[:interval]
|
131
|
+
|
132
|
+
discipline[:extra_sessions_allowable] += service[:extra_sessions_allowable].to_i
|
133
|
+
|
134
|
+
discipline[:interval_for_extra_sessions_allowable] = service[:interval_for_extra_sessions_allowable]
|
135
|
+
end
|
136
|
+
|
137
|
+
discipline
|
138
|
+
end
|
139
|
+
|
140
|
+
def same_interval(services)
|
141
|
+
interval = services[0].nil? ? "" : services[0][:interval]
|
142
|
+
same = true
|
143
|
+
|
144
|
+
services.each do |service|
|
145
|
+
if interval != service[:interval]
|
146
|
+
# puts "this happened for real? interval #{interval} and service interval #{service[:interval]} #{services}"
|
147
|
+
same = false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
same
|
152
|
+
end
|
153
|
+
|
154
|
+
def normalize_to_monthly_frequency(services)
|
155
|
+
# average business days for each interval
|
156
|
+
interval_average_days = {
|
157
|
+
"weekly" => 5,
|
158
|
+
"monthly" => 22,
|
159
|
+
"yearly" => 210 # take away average holidays period with is 2.5 months
|
160
|
+
}
|
161
|
+
|
162
|
+
return services if same_interval(services)
|
163
|
+
|
164
|
+
services.map do |service|
|
165
|
+
if !(service[:interval] == "monthly")
|
166
|
+
# weekly(5 days) = frequency # weekly
|
167
|
+
# monthly(20 days) = frequency * monthly
|
168
|
+
# yearly(200 days)
|
169
|
+
|
170
|
+
f = service[:frequency]
|
171
|
+
|
172
|
+
service[:frequency] = ((service[:frequency] * interval_average_days["monthly"].to_f) / interval_average_days[service[:interval]]).round
|
173
|
+
|
174
|
+
service[:interval] = "monthly"
|
175
|
+
end
|
176
|
+
|
177
|
+
service
|
178
|
+
end
|
179
|
+
|
180
|
+
services
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_date(date)
|
184
|
+
begin
|
185
|
+
Date.strptime(date, '%m-%d-%Y')
|
186
|
+
rescue => exception
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def disciplines_cleaner(disciplines)
|
191
|
+
# use the fake arbitrary reset date to remove unrequired disciplines
|
192
|
+
disciplines.filter { |discipline| !discipline.empty? }
|
193
|
+
end
|
194
|
+
|
195
|
+
def active_services(services)
|
196
|
+
services.filter do |school_plan_service|
|
197
|
+
within = true
|
198
|
+
begin
|
199
|
+
if !(parse_date(school_plan_service[:start_date]) <= parse_date(date) && parse_date(date) <= parse_date(school_plan_service[:end_date]))
|
200
|
+
within = false
|
201
|
+
end
|
202
|
+
rescue => exception
|
203
|
+
end
|
204
|
+
|
205
|
+
within
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|