pacing 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9682bf40847898cd9daf1230c6c71306fd28286ed513450383e68236c667d305
4
- data.tar.gz: 5fbdcdfbb13eca9854befe9e1aa297a6475cec918f8b1e87242fc419a05cadf6
3
+ metadata.gz: 3c9f1bb20d57079d96b0bf851745a047e274cc2ac91698dafb61ddc34800875d
4
+ data.tar.gz: b83584cd4d641299955cb4b28666d742e17d6ac90ab0168bdf9898e3b20f7307
5
5
  SHA512:
6
- metadata.gz: d9299ed6be2821c309b056025559bc1fc871ef3de6554772d1ad590f92e2605089d769dc4bc24be937e207e33df462e7a5a1acb02aa43904458c6ba2ae7ce199
7
- data.tar.gz: f0ab3cf08f73ae25b9caacf6a971bef108ca00068aa2a3f9d00bf57de22bf8c3ac26bb6a3e19924f7bf02d3d0c4766d80f1fcecc0e127a23c4e055e616d0892b
6
+ metadata.gz: 7f9df40d14c5428d64a4465930a675f803eb428d1ccc0602de23e591fd3225993c8c37b9e3437e1954885cd07584d29c7d447d93e2fda4d9d94df4cae0da60a6
7
+ data.tar.gz: ed423571438c677b40fde49964c6866fd63a40c763a93c2bcac563f9547c12ad64d5371a4ef705d97f35f2b7594b34dd00b0ee70d5951d29aec1ab05a3b19172
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .DS_Store
16
+ .vscode/launch.json
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.6
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "holidays", "~> 8.6"
6
+
7
+ group :test do
8
+ gem 'rspec'
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Ambitious Idea Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Pacing
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 Services plan. This gem helps to calculate remaining visits as well as a therapist's current pace to meet visit mandates.
4
+
5
+ + 🐇 Ahead of pace
6
+ + 😁 On pace
7
+ + 🐢 Behind pace
8
+
9
+ ## Getting started
10
+
11
+ **Ruby**
12
+
13
+ *Supports Ruby x.x.x and above*
14
+ ```
15
+ gem install pacing
16
+ ```
17
+
18
+ **Ruby on Rails**
19
+
20
+ Add this line to your application’s Gemfile:
21
+ ```ruby
22
+ gem 'pacing'
23
+ ```
24
+
25
+ ## How it works
26
+
27
+ ```ruby
28
+
29
+ school_plan = {
30
+ school_plan_services: [
31
+ {
32
+ school_plan_type: "IEP", # string ('IEP', '504 Plan', 'Services Plan' )
33
+ start_date: "01-01-2022", # string (mm-dd-yyyy)
34
+ end_date: "01-01-2023", # string (mm-dd-yyyy)
35
+ type_of_service: "Language Therapy", # string ('Language Therapy', 'Speech Therapy', 'Occupation Therapy', 'Physical Therapy', 'Feeding Therapy', 'Speech and Language Therapy')
36
+ frequency: 6, # integer
37
+ interval: "monthly", # string ('weekly', 'monthly', 'yearly')
38
+ time_per_session_in_minutes: 30, # integer
39
+ completed_visits_for_current_interval: 7, # integer
40
+ extra_sessions_allowable: 1, # integer
41
+ interval_for_extra_sessions_allowable: "monthly", # string ('weekly', 'monthly', 'yearly')
42
+ },
43
+ {
44
+ school_plan_type: "IEP", # string ('IEP', '504 Plan', 'Services Plan' )
45
+ start_date: "01-01-2022", # string (mm-dd-yyyy)
46
+ end_date: "01-01-2023", # string (mm-dd-yyyy)
47
+ type_of_service: "Physical Therapy", # string ('Language Therapy', 'Speech Therapy', 'Occupation Therapy', 'Physical Therapy', 'Feeding Therapy', 'Speech and Language Therapy')
48
+ frequency: 6, # integer
49
+ interval: "monthly", # string ('weekly', 'monthly', 'yearly')
50
+ time_per_session_in_minutes: 30, # integer
51
+ completed_visits_for_current_interval: 7, # integer
52
+ extra_sessions_allowable: 1, # integer
53
+ interval_for_extra_sessions_allowable: "monthly", # string ('weekly', 'monthly', 'yearly')
54
+ }
55
+ ]
56
+ }
57
+
58
+ date = '01-22-2022' # string (mm-dd-yyyy)
59
+ non_business_days = ['01-25-2022'] # array of strings (mm-dd-yyyy)
60
+ paced = Pacing::Pacer.new(school_plan: school_plan, date: date, non_business_days: non_business_days)
61
+ paced.calculate
62
+
63
+ =begin
64
+ => {
65
+ school_plan_services: [
66
+ {
67
+ school_plan_type: "IEP",
68
+ start_date: "01-01-2022",
69
+ end_date: "01-01-2023",
70
+ type_of_service: "Language Therapy",
71
+ frequency: 6,
72
+ interval: "monthly",
73
+ time_per_session_in_minutes: 30,
74
+ completed_visits_for_current_interval: 7,
75
+ extra_sessions_allowable: 1,
76
+ interval_for_extra_sessions_allowable: "monthly",
77
+ pace: 3,
78
+ reset_date: "01-31-2022",
79
+ remaining_visits: 0,
80
+ pace_indicator: "🐇"
81
+ },
82
+ {
83
+ school_plan_type: "IEP",
84
+ start_date: "01-01-2022",
85
+ end_date: "01-01-2023",
86
+ type_of_service: "Physical Therapy",
87
+ frequency: 6,
88
+ interval: "monthly",
89
+ time_per_session_in_minutes: 30,
90
+ completed_visits_for_current_interval: 7,
91
+ extra_sessions_allowable: 1,
92
+ interval_for_extra_sessions_allowable: "monthly",
93
+ pace: 3,
94
+ reset_date: "01-31-2022",
95
+ remaining_visits: 0,
96
+ pace_indicator: "🐇"
97
+ }
98
+ ]
99
+ }
100
+ =end
101
+
102
+ ```
103
+
104
+ ## Data Types
105
+
106
+ Pacing accepts input which consists of a school_plan, a date and a non_business_day variable. The school_plan variable is a hash that includes the various school plan services that the client received, the date is a string and the non_business_days variable is an array of dates.
107
+
108
+ The output received is a hash that contains all the necessary information that is useful to the user.
109
+
110
+ The following list shows the various variables and what they consist of:
111
+
112
+ 1. Input
113
+ - `school_plan_type` is a string.
114
+ - `start_date` is a string.
115
+ - `end_date` is a string.
116
+ - `type_of_service` is a string.
117
+ - `frequency` is an integer.
118
+ - `interval` is a string.
119
+ - `time_per_session_in_minutes` is an integer.
120
+ - `extra_sessions_allowable` is an integer.
121
+ - `interval_for_extra_sessions_allowable` is a string.
122
+ 2. Output
123
+ - `school_plan_type` is a string.
124
+ - `start_date` is a string.
125
+ - `end_date` is a string.
126
+ - `type_of_service` is a string.
127
+ - `frequency` is an integer.
128
+ - `interval` is a string.
129
+ - `time_per_session_in_minutes` is an integer.
130
+ - `extra_sessions_allowable` is an integer.
131
+ - `interval_for_extra_sessions_allowable` is a string.
132
+ - `reset_date` is a string.
133
+ - `pace` is an integer.
134
+ - `pace_indicator` is a string.
135
+
136
+ ## Terminology
137
+
138
+ + **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).
139
+ + **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.
140
+ + **Services Plan**: A plan paid for by the local school district for students with disabilities who attend private schools. A services plan does not have to ensure a child is provided with FAPE (free appropriate public education). A services 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.
141
+
142
+ ## Testing
143
+
144
+ + `bundle exec rspec`
145
+
146
+ ## Contributing
147
+
148
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
149
+
150
+ + [Report bugs](https://github.com/Ambiki/pacing/issues)
151
+ + Fix bugs and [submit pull requests](https://github.com/Ambiki/pacing/pulls)
152
+ + Write, clarify, or fix documentation
153
+ + Suggest or add new features
154
+
155
+ To get started with development:
156
+
157
+ 1. Fork it ( https://github.com/Ambiki/pacing/fork )
158
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
159
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
160
+ 4. Push to the branch (`git push origin my-new-feature`)
161
+ 5. Create a new Pull Request
162
+
163
+ ## Publishing
164
+
165
+ 1. gem build pacing.gemspec
166
+ 2. gem push pacing-x.x.x.gem
167
+
168
+ ## License
169
+
170
+ The MIT License (MIT)
171
+
172
+ Copyright (c) 2022 Ambitious Idea Labs
173
+
174
+ Permission is hereby granted, free of charge, to any person obtaining
175
+ a copy of this software and associated documentation files (the
176
+ "Software"), to deal in the Software without restriction, including
177
+ without limitation the rights to use, copy, modify, merge, publish,
178
+ distribute, sublicense, and/or sell copies of the Software, and to
179
+ permit persons to whom the Software is furnished to do so, subject to
180
+ the following conditions:
181
+
182
+ The above copyright notice and this permission notice shall be
183
+ included in all copies or substantial portions of the Software.
184
+
185
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
186
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
187
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
188
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
189
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
190
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
191
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
@@ -0,0 +1,215 @@
1
+ require 'date'
2
+ require 'holidays'
3
+
4
+ module Pacing
5
+ class Pacer
6
+ COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
7
+ attr_reader :school_plan, :date, :non_business_days, :state
8
+
9
+ def initialize(school_plan:, date:, non_business_days:, state: :us_tn)
10
+ @school_plan = school_plan
11
+ @non_business_days = non_business_days
12
+ @date = date
13
+ @state = state
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
+
21
+ @non_business_days.each do |non_business_day|
22
+ 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)
23
+ end
24
+
25
+ @school_plan[:school_plan_services].each do |school_plan_service|
26
+ 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?
27
+
28
+ 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?
29
+
30
+ 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])
31
+
32
+ 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])
33
+
34
+ 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?
35
+
36
+ raise TypeError.new("Frequency must be an integer and cannot be nil") if school_plan_service[:frequency].class != Integer || school_plan_service[:frequency].nil?
37
+
38
+ raise TypeError.new("Interval must be a string and cannot be nil") if school_plan_service[:interval].class != String || school_plan_service[:interval].nil?
39
+
40
+ 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?
41
+
42
+ 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?
43
+
44
+ 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?
45
+
46
+ 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?
47
+ end
48
+ end
49
+
50
+ def calculate
51
+ services = @school_plan[:school_plan_services]
52
+
53
+ services = services.map do |service|
54
+ expected = expected_visits(start_date: parse_date(service[:start_date]), end_date: parse_date(service[:end_date]), frequency: service[:frequency], interval: service[:interval])
55
+
56
+ service[:pace] = pace(service[:completed_visits_for_current_interval], expected)
57
+ service[:reset_date] = reset_date(start_date: service[:start_date], interval: service[:interval_for_extra_sessions_allowable])
58
+ service[:remaining_visits] = remaining_visits(completed_visits: service[:completed_visits_for_current_interval], required_visits: service[:frequency])
59
+ service[:pace_indicator] = pace_indicator(service[:pace])
60
+
61
+ service
62
+ end
63
+
64
+ { school_plan_services: services }
65
+ end
66
+
67
+ # get a spreadout of visit dates over an interval by using simple proportion.
68
+ def expected_visits(start_date:, end_date:, frequency:, interval:)
69
+ reset_start = start_of_treatment_date(start_date, interval)
70
+
71
+ reset_end = reset_start + interval_days(interval)
72
+
73
+ days_between = business_days(reset_start, reset_end).count
74
+
75
+ days_passed = business_days(reset_start, parse_date(@date)).count
76
+
77
+ if days_between != 0
78
+ return ((frequency/days_between.to_f) * days_passed).round
79
+ else
80
+ return 0
81
+ end
82
+ end
83
+
84
+ def interval_days(interval)
85
+ case interval
86
+ when "monthly"
87
+ return COMMON_YEAR_DAYS_IN_MONTH[(parse_date(@date)).month]
88
+ when "weekly"
89
+ return 6
90
+ when "yearly"
91
+ return parse_date(@date).leap? ? 366 : 365
92
+ end
93
+ end
94
+
95
+ def pace(actual_visits, expected_visits)
96
+ # Pace = actual visits at point in time - expected visits to meet frequency at that point in time
97
+ actual_visits - expected_visits
98
+ end
99
+
100
+ def pace_indicator(pace)
101
+ if pace == 0
102
+ return "😁"
103
+ elsif pace > 0
104
+ return "🐇"
105
+ elsif pace < 0
106
+ return "🐢"
107
+ end
108
+ end
109
+
110
+ def parse_date(date)
111
+ Date.strptime(date, '%m-%d-%Y')
112
+ end
113
+
114
+ def parsed_non_business_days
115
+ @non_business_days.map do |day|
116
+ parse_date(day)
117
+ end
118
+ end
119
+
120
+ # days on which a session can hold
121
+ def business_days(start_date, end_date)
122
+ # remove saturdays and sundays
123
+ # remove holidays(array from Ambiki)
124
+ # remove school holidays/non business days
125
+ # should we remove today?(datetime call?)
126
+ working_days = get_working_days(start_date, end_date)
127
+
128
+ holidays = get_holidays(start_date, end_date)
129
+
130
+ working_days.sort - parsed_non_business_days.sort - holidays.sort
131
+ end
132
+
133
+ # scoped to the interval
134
+ def remaining_visits(completed_visits:, required_visits:)
135
+ visits = required_visits.to_i - completed_visits.to_i
136
+ visits = 0 if visits < 0
137
+ visits
138
+ end
139
+
140
+ # scoped to the interval
141
+ def reset_date(start_date:, interval:)
142
+ case interval
143
+ when "monthly"
144
+ return reset_date_monthly(start_date, interval)
145
+ when "weekly"
146
+ return reset_date_weekly(start_date, interval)
147
+ when "yearly"
148
+ return reset_date_yearly(start_date)
149
+ end
150
+ end
151
+
152
+ # scoped to the interval
153
+ def start_of_treatment_date(start_date, interval="monthly")
154
+ case interval
155
+ when "monthly"
156
+ return start_of_treatment_date_monthly(start_date)
157
+ when "weekly"
158
+ return start_of_treatment_date_weekly(start_date)
159
+ when "yearly"
160
+ return start_of_treatment_date_yearly(start_date)
161
+ end
162
+ end
163
+
164
+ def get_working_days(start_date, end_date)
165
+ (start_date..end_date).filter do |day|
166
+ !(day.saturday? || day.sunday?)
167
+ end
168
+ end
169
+
170
+ def get_holidays(start_date, end_date)
171
+ Holidays.between(start_date, end_date, @state).map { |holiday| holiday[:date] }
172
+ end
173
+
174
+ # get actual date of the first day of the week where date falls
175
+ def week_start(date, offset_from_sunday=0)
176
+ date - ((date.wday - offset_from_sunday)%7)
177
+ end
178
+
179
+ # reset date for the yearly interval
180
+ def reset_date_yearly(start_date)
181
+ (parse_date(@date).leap? ? parse_date(start_date) + 366 : parse_date(start_date) + 365).strftime("%m-%d-%Y")
182
+ end
183
+
184
+ # reset date for the monthly interval
185
+ def reset_date_monthly(start_date, interval)
186
+ (start_of_treatment_date(parse_date(start_date), interval) + COMMON_YEAR_DAYS_IN_MONTH[(parse_date(@date)).month]-1).strftime("%m-%d-%Y")
187
+ end
188
+
189
+ # reset date for the weekly interval
190
+ def reset_date_weekly(start_date, interval)
191
+ (start_of_treatment_date(parse_date(start_date), interval) + 7).strftime("%m-%d-%Y")
192
+ end
193
+
194
+ # start of treatment for the yearly interval
195
+ def start_of_treatment_date_yearly(start_date)
196
+ parse_date("#{start_date.month}-#{start_date.day}-#{parse_date(@date).year}")
197
+ end
198
+
199
+ # start of treatment for the montly interval
200
+ def start_of_treatment_date_monthly(start_date)
201
+ parse_date("#{parse_date(@date).month}-#{start_date.day}-#{parse_date(@date).year}")
202
+ end
203
+
204
+ # start of treatment for the weekly interval
205
+ def start_of_treatment_date_weekly(start_date)
206
+ date = parse_date(@date)
207
+
208
+ week_start_date = week_start(date)
209
+ weekly_date = week_start_date + start_date.wday
210
+ weekly_date = date < weekly_date ? weekly_date - 7 : weekly_date
211
+
212
+ weekly_date
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pacing
4
+ VERSION = "0.1.3"
5
+ end
data/pacing.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'pacing/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'pacing'
7
+ s.version = Pacing::VERSION
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."
10
+ s.authors = ["Kevin S. Dias", "Samuel Okoth", "Lukong I. Nsoseka"]
11
+ s.email = 'info@ambiki.com'
12
+ s.files = `git ls-files -z`.split("\x0")
13
+ s.homepage = 'https://github.com/Ambiki/pacing'
14
+ s.license = 'MIT'
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_development_dependency "rspec"
19
+ s.add_runtime_dependency "holidays", '~> 8.6', '>= 8.6'
20
+ end
21
+