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
14 A program to count the number of school days and distribute milestones amongst the term.
20 # From https://stackoverflow.com/a/45349125/2755116
21 def nicopartition(n
: int, d
: int, depth
=0) -> list[int]:
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
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
]:
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.
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
]
65 wexcl
= list(set(cperiod
) - set(excluding
))
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"""
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
]:
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)`.
83 if len(days
) < nmilestones
:
84 # In this case, we return all days, because we need more than we have
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`
94 gap_length
= math
.floor((duration
-nmilestones
)/nmilestones
)
95 rest
= duration
- (gap_length
*nmilestones
+ nmilestones
)
97 # Setting initial gaps as [gap_length, .... , gap_length]
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).
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`
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)
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'])
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
138 # Number of milestones we desired to do
141 # Grace period. In the interval [start, start+startgrace] and [end-endgrace, end] will not set milestones
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
]))