cbr-schedule

An unofficial Bible-reading-plan generator
git clone git://git.danielmoch.com/cbr-schedule.git
Log | Files | Refs | README | LICENSE

commit 3b8afdabcab2f058d1f10892b7b510729d2301cd
parent b937c3dca9aeec657b9ea0b7d9c8709507acfcf0
Author: Daniel Moch <daniel@danielmoch.com>
Date:   Sat, 24 Feb 2018 19:07:03 -0500

Add generate_schedule and support files

This top-level executable take the json input files generated by
generate_readings and builds a reading schedule. The details of the plan
have been pretty well parameterized into a config file (schedule.ini).

There is still some refactoring to do, but I'm committing now since the
utility works.

UNHANDLED EDGE CASE: Leap years

Diffstat:
Agenerate_schedule | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Areadingplan.py | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aschedule.ini | 30++++++++++++++++++++++++++++++
3 files changed, 281 insertions(+), 0 deletions(-)

diff --git a/generate_schedule b/generate_schedule @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Daniel Moch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +from readingplan import ReadingPlan +from sys import argv +import configparser + +def print_usage(): + print('Usage: generate_schedule [config]') + print('\tconfig - Path to schedule config file (default: schedule.ini)') + +def generate_schedule(configfile='schedule.ini'): + """ + Generate the reading plan, using the ReadingPlan class as a + generator. This function takes the day-of-week that January 1st + falls on as its single parameter. + """ + config = configparser.ConfigParser() + config.read(configfile) + plan = ReadingPlan() + plan.build_plan(config) + while not plan.done: + print(plan.next_reading()) + +if __name__ == "__main__": + if len(argv) > 2: + print_usage() + exit(1) + elif len(argv) < 2: + generate_schedule() + else: + generate_schedule(argv[1]) diff --git a/readingplan.py b/readingplan.py @@ -0,0 +1,198 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Daniel Moch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +import json + +DAYS_OF_WEEK = ( + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ) + +MONTHS = ( + ('January', 31,), + ('February', 28,), + ('March', 31,), + ('April', 30,), + ('May', 31,), + ('June', 30,), + ('July', 31,), + ('August', 31,), + ('September', 30,), + ('October', 31,), + ('November', 30,), + ('December', 31,), + ) + +class ReadingPlan: + """ + Once initialized, maintain state of the plan so the schedule + function can simply ask for the next day's reading. + """ + _initialized = False + _current_month = 0 + _current_day = 1 + _day_of_week = 0 + + _sections = {} + _days_sections = {} + _days_sections_fallback = {} + _reading_dictionaries = {} + _reading_indices = {} + done = False + + def _general_init(self, config): + jan1_day_of_week = config['general']['jan1_day_of_week'] + for idx, day in enumerate(DAYS_OF_WEEK): + if day.upper() == jan1_day_of_week.upper(): + print('Setting _day_of_week to ' + str(idx)) + self._day_of_week = idx + break + + def _days_sections_init(self, config, section): + for day in DAYS_OF_WEEK: + day_key = day.lower() + if day_key in config[section].keys(): + self._days_sections[day] = config[section][day_key] + else: + print('No readings planned for ' + day + 's') + self._days_sections[day] = None + + def _days_sections_fallback_init(self, config, section): + for day in DAYS_OF_WEEK: + day_key = day.lower() + if day_key in config[section].keys(): + self._days_sections_fallback[day] = \ + config[section][day_key] + else: + print('No readings planned for ' + day + 's') + self._days_sections_fallback[day] = None + + def _section_init(self, config, section): + if 'name' in config[section].keys(): + self._sections[section] = config[section]['name'] + else: + print('No section name specified for ' + section) + print('Using ' + section + ' as name') + self._sections[section] = section + reading_dictionary = [] + with open('sections/' + section + '.json', 'r') as sfile: + reading_dictionary = json.load(sfile) + self._reading_dictionaries[section] = reading_dictionary + if 'book' in config[section].keys(): + book = config[section]['book'] + if 'chapter' in config[section].keys(): + chapter = config[section]['chapter'] + else: + chapter = '1' + for dictionary in reading_dictionary: + if dictionary['book'] == book and \ + dictionary['reading'] == chapter: + self._reading_indices[section] = \ + dictionary['index'] + if section not in self._reading_indices.keys(): + self._reading_indices[section] = 0 + + def _print_plan_info(self): + print('_sections:' + str(self._sections.keys())) + print('_reading_dictionaries: ' + \ + str(self._reading_dictionaries.keys())) + print('_reading_indices: ' + str(self._reading_indices.values())) + print('_day_of_week: ' + str(self._day_of_week)) + print('_days_sections: ' + str(self._days_sections.keys())) + print('_days_sections_fallback: ' + + str(self._days_sections_fallback.values())) + + def build_plan(self, config): + for section in config.sections(): + if section == 'general': + self._general_init(config) + elif section == 'days_sections': + self._days_sections_init(config, section) + elif section == 'days_sections_fallback': + self._days_sections_fallback_init(config, section) + else: + self._section_init(config, section) + self._print_plan_info() + self._initialized = True + + def _next_day(self): + if self._current_day == MONTHS[self._current_month][1]: + self._current_day = 1 + self._current_month += 1 + if self._current_month == len(MONTHS): + self.done = True + else: + self._current_day += 1 + + last_day = len(DAYS_OF_WEEK) - 1 + if self._day_of_week == last_day: + self._day_of_week = 0 + else: + self._day_of_week += 1 + + def _readings(self, sections): + readings = [] + if sections is not None: + for section in sections.split(','): + if section not in self._sections.keys(): + raise ValueError(section + ' is not a known section') + + section_dict = self._reading_dictionaries[section] + for reading_dict in section_dict: + if reading_dict['index'] == self._reading_indices[section]: + reading = {} + reading['section'] = self._sections[section] + reading['book'] = reading_dict['book'] + reading['reading'] = reading_dict['reading'] + readings.append(reading) + self._reading_indices[section] += 1 + break + return readings + + def next_reading(self): + if not self._initialized: + raise RuntimeError('Attempting to use an uninitialized ReadingPlan') + results = {} + readings = [] + today_sections = self._days_sections[DAYS_OF_WEEK[self._day_of_week]] + today_sections_fallback = \ + self._days_sections_fallback[DAYS_OF_WEEK[self._day_of_week]] + for reading in self._readings(today_sections): + readings.append(reading) + + if readings == []: + for reading in self._readings(today_sections_fallback): + readings.append(reading) + results = { + 'day_of_week': self._day_of_week, + 'day': self._current_day, + 'month': MONTHS[self._current_month][0], + 'readings': readings + } + self._next_day() + return results diff --git a/schedule.ini b/schedule.ini @@ -0,0 +1,30 @@ +[general] +jan1_day_of_week=Monday + +[new_testament] +name=New Testament + +[old_testament] +name=Old Testament +book=Ecclesiastes +chapter=12 + +[psalms] +name=Psalms +book=Psalm +chapter=105 + +[days_sections] +monday=new_testament,old_testament +tuesday=new_testament,old_testament +wednesday=new_testament,old_testament +thursday=new_testament,old_testament +friday=new_testament,old_testament +saturday=psalms + +[days_sections_fallback] +monday=psalms +tuesday=psalms +wednesday=psalms +thursday=psalms +friday=psalms