~todo
[soltea.git] / milestones.py
blob06c9871a43abaa75caaaa2957c7b79869c6a5753
1 #!/usr/bin/env python
2 #
3 # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5 # You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7 # This program has the aim to count the number of school days
8 # and distribute milestones amongst the term
9 # Milestones are what you want: exams, presentations, change of unit, etc.
11 # You can use `pdoc --math <this file>` to view API documentation
13 """
14 A program to count the number of school days and distribute milestones amongst the term.
15 """
17 import datetime
18 import math
20 # From https://stackoverflow.com/a/45349125/2755116
21 def nicopartition(n: int, d: int, depth=0) -> list[int]:
22 """
23 An auxiliary function in order to calculate the list of (number) partitions of $n$ into $d$ non-negative parts with permutations.
24 The implementation is original from [Nico Schlömer](https://stackoverflow.com/users/353337/nico-schl%c3%b6mer):
25 You can see the original source code [here](https://stackoverflow.com/a/45349125/2755116) which is licensed under
26 Creative Commons CC-BY-SA 3.0
27 """
29 if d == depth:
30 return [[]]
31 return [
32 item + [i]
33 for i in range(n+1)
34 for item in nicopartition(n-i, d, depth=depth+1)
37 def partitions(n: int, d: int) -> list[int]:
38 """Returns the (number) partitions of $n$ into $d$ non-negative parts with permutations using `nicopartition` procedure"""
40 return [[n-sum(p)] + p for p in nicopartition(n, d-1)]
42 def period(start: datetime.date, end: datetime.date) -> list[datetime.date]:
43 """Returns the list of dates between `start` and `end` both included"""
45 return [start + datetime.timedelta(days=i) for i in range(0, (end-start).days+1)]
47 def days(start: datetime.date, end: datetime.date, excluding: list[datetime.date], timetable: list[int]) -> list[datetime.date]:
48 r"""
49 Returns the list of days `d` which satisfies:
51 - $ start \leq d \leq end $
52 - $ d \not \in excluding $
53 - $ d.isoweekday \in timetable$.
55 Example: `timetable = [1, 2, 5]` means we select days being Monday, Tuesday and Friday.
56 """
58 # The interval of time between `self.start` and `self.end`
59 rawperiod = period(start, end)
61 # select only the days in the timetable
62 cperiod = [d for d in rawperiod if d.isoweekday() in timetable]
64 # excluding the days
65 wexcl = list(set(cperiod) - set(excluding))
67 return sorted(wexcl)
69 def select(days: list[datetime.date], gaps: list[int]) -> list[datetime.date]:
70 """Returns a sublist `[d_i]` of `days` such that before day $d_i$, there are $g_i$ days before. That is, `d_0 = days[g_0 + 1]`, `d_1 = days[g_0 + 1 + g_1 + 1]`, and so on"""
72 c = len(gaps)
73 positions = [i + 1 + sum([gaps[j] for j in range(0, i+1)]) for i in range(0, c)]
74 return [days[i-1] for i in positions]
77 def milestones(days: list[datetime.date], nmilestones: int = 5) -> list[datetime.date]:
78 """
79 Returns the dates on which the milestones would happen. We plan to have `nmilestones` milestones in the set of `days`.
80 We calculate `g = math.floor((len(days)-nmilestones)/nmilestones)`.
81 """
83 if len(days) < nmilestones:
84 # In this case, we return all days, because we need more than we have
85 return days
86 else:
87 # In this case, we make a partition:
88 # days = gap_1, milestone_1, gap_2, milestone_2, ... gap_s, milestone_s, rest
89 # where s = nmilestones
90 # But the `rest` could be distributed amongst each `gap_i`
92 # Calculate the gaps' length and the `rest`
93 duration = len(days)
94 gap_length = math.floor((duration-nmilestones)/nmilestones)
95 rest = duration - (gap_length*nmilestones + nmilestones)
97 # Setting initial gaps as [gap_length, .... , gap_length]
98 igaps = []
99 for i in range(0, nmilestones):
100 igaps.append(gap_length)
102 # Calculate the possible gaps distributing `rest` amongst all gaps
103 # So we sum igaps + p for p in partitions(rest, nmilestones).
104 gaps = []
105 for p in partitions(rest, nmilestones):
106 gaps.append([x + y for x, y in zip(igaps, p)])
108 # Calculate the milestones using `select` procedure with `gaps`
109 milestones = []
111 for p in gaps:
112 milestones.append(select(days, p))
114 # Returns sorted milestones
115 return sorted(milestones)
118 if __name__ == "__main__":
119 # Start and end of Course
120 start = datetime.date(2023,10,2)
121 end = datetime.date(2024,1,25)
123 # Excluding days
124 ## vacation
125 xmas = {'start': datetime.date(2023,12,22), 'end': datetime.date(2024,1,7)}
126 easter = {'start': datetime.date(2024,3,28), 'end': datetime.date(2024,4,7)}
127 vacation = period(xmas['start'], xmas['end']) + period(easter['start'], easter['end'])
129 ## holidays
130 holidays = [datetime.date(2023,10,12), datetime.date(2023,11,1), datetime.date(2023,12,6), datetime.date(2023,12,7), datetime.date(2023,12,8), datetime.date(2024,2,29), datetime.date(2024,3,1), datetime.date(2024,3,4), datetime.date(2024,5,1), datetime.date(2024,5,2), datetime.date(2024,5,3)]
132 excluding = holidays + vacation
134 # Days of the week in which the course takes place
135 # 'Monday' = 1, ... 'Sunday' = 7
136 timetable = [2,4,5]
138 # Number of milestones we desired to do
139 nmilestones = 4
141 # Grace period. In the interval [start, start+startgrace] and [end-endgrace, end] will not set milestones
142 startgrace = 1
143 endgrace = 2
145 # Conversion of days of the week
146 daysofweek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
148 # Calculate days and milestones
149 days = days(start, end, excluding, timetable)
150 ## Be aware of grace time period
151 milestones = milestones(days[(0+startgrace):(len(days)-endgrace)], nmilestones)
153 print("The course starts on {start} and ends on {end} and takes place on {timetable}".format(start=start, end=end, timetable=[daysofweek[s-1] for s in timetable]))
154 print("The course has {duration} school days: {sdays}".format(duration=len(days), sdays=[str(s) for s in days]))
155 print("We plan to do {nmilestones} milestones with a grace time of {startgrace} days at the beginning and {endgrace} days at the end: {milestones}".format(nmilestones=nmilestones, startgrace=startgrace, endgrace=endgrace, milestones=[[str(d) for d in s] for s in milestones]))