@fineanmol/holiday-optimizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fineanmol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Holiday Optimizer 🏖️
2
+
3
+ Optimize your vacation days using Dynamic Programming! Maximize your total days off by strategically placing breaks around weekends and public holidays.
4
+
5
+ **Live Demo:** [Try it here](https://fineanmol.github.io/holiday-optimizer/)
6
+
7
+ ## Install (npm)
8
+
9
+ ```bash
10
+ npm install @fineanmol/holiday-optimizer
11
+ ```
12
+
13
+ ```javascript
14
+ import { optimize, formatReport, getPreset } from "@fineanmol/holiday-optimizer";
15
+
16
+ const params = getPreset("germany");
17
+ const result = optimize(params);
18
+ console.log(formatReport(result, params));
19
+ ```
20
+
21
+ ## What It Does
22
+
23
+ This tool helps you plan your vacations optimally. Given:
24
+
25
+ - Your total paid leave days
26
+ - Public holidays in your country
27
+ - Your preferences (minimum/maximum break length, spacing between breaks)
28
+
29
+ It finds the best schedule that maximizes your total days off by leveraging weekends and holidays.
30
+
31
+ ## Features
32
+
33
+ - 🎯 **Dynamic Programming Algorithm** - Finds optimal vacation schedule
34
+ - 🌍 **Multi-Country Support** - Pre-configured holidays for Germany and India (easily extensible)
35
+ - ⚙️ **Customizable Constraints** - Set your own min/max break lengths and spacing
36
+ - 📊 **Detailed Reports** - See exactly which dates to take off and how many days you'll get
37
+ - 💻 **Zero Server Cost** - Runs entirely in the browser using GitHub Pages
38
+
39
+ ## How It Works
40
+
41
+ The optimizer uses three key techniques:
42
+
43
+ 1. **Dominance Pruning** - For each starting day, keeps only the most efficient break options
44
+ 2. **Binary Search** - Efficiently finds next valid break that satisfies spacing constraints
45
+ 3. **Dynamic Programming** - Optimizes for maximum total days off within your constraints
46
+
47
+ ## Usage
48
+
49
+ 1. Select your country from the dropdown
50
+ 2. Set your preferences:
51
+ - Year to optimize
52
+ - Start date
53
+ - Total paid leave days
54
+ - Minimum break length (days)
55
+ - Maximum break length (days)
56
+ - Time between breaks (days)
57
+ 3. Click "Calculate Optimal Schedule"
58
+ 4. Review your optimized vacation plan!
59
+
60
+ ## Example
61
+
62
+ With **19 paid leave days** and default settings:
63
+
64
+ - **Result:** ~52 total days off
65
+ - **Breaks:** ~12 breaks throughout the year
66
+ - **Distribution:** Approximately 1 break per month
67
+
68
+ ## Adding New Countries
69
+
70
+ To add holidays for a new country, edit `country-presets.js`:
71
+
72
+ ```javascript
73
+ newCountry: {
74
+ name: "Country Name",
75
+ year: 2026,
76
+ defaultPTO: 10,
77
+ holidays: [
78
+ { date: "2026-01-01", name: "New Year's Day" },
79
+ // ... more holidays
80
+ ]
81
+ }
82
+ ```
83
+
84
+ The country will automatically appear in the dropdown!
85
+
86
+ ## Algorithm Details
87
+
88
+ - **Time Complexity:** O(n × m × P) where n = candidates, m = max break length, P = PTO days
89
+ - **Space Complexity:** O(n × P) for DP table
90
+ - **Optimization Goal:** Maximize total days off (including weekends and holidays)
91
+
92
+ ## Tech Stack
93
+
94
+ - Pure JavaScript (ES6 modules)
95
+ - No dependencies
96
+ - Works in all modern browsers
97
+ - GitHub Pages compatible
98
+
99
+ ## Inspiration
100
+
101
+ Inspired by [Ankit's vacation optimizer](https://gist.github.com/ag5826000/478d3607df0eff50278d57429f2308e9) - converted from Python to JavaScript for browser deployment.
102
+
103
+ ## License
104
+
105
+ MIT License - feel free to use and modify!
106
+
107
+ ---
108
+
109
+ **Made with ❤️ to help you maximize your vacation time!**
@@ -0,0 +1,104 @@
1
+ export const countryPresets = {
2
+ germany: {
3
+ name: "Germany (Berlin)",
4
+ year: 2026,
5
+ defaultPTO: 19,
6
+ holidays: [
7
+ { date: "2026-01-01", name: "Neujahr" },
8
+ { date: "2026-03-08", name: "Internationaler Frauentag (Berlin)" },
9
+ { date: "2026-04-03", name: "Karfreitag" },
10
+ { date: "2026-04-06", name: "Ostermontag" },
11
+ { date: "2026-05-01", name: "Tag der Arbeit" },
12
+ { date: "2026-05-14", name: "Christi Himmelfahrt" },
13
+ { date: "2026-05-25", name: "Pfingstmontag" },
14
+ { date: "2026-10-03", name: "Tag der Deutschen Einheit" },
15
+ { date: "2026-10-31", name: "Reformationstag (Berlin)" },
16
+ { date: "2026-12-25", name: "1. Weihnachtstag" },
17
+ { date: "2026-12-26", name: "2. Weihnachtstag" },
18
+ ],
19
+ },
20
+
21
+ india: {
22
+ name: "India",
23
+ year: 2026,
24
+ defaultPTO: 10,
25
+ holidays: [
26
+ { date: "2026-01-26", name: "Republic Day" },
27
+ { date: "2026-03-08", name: "Holi" },
28
+ { date: "2026-03-29", name: "Good Friday" },
29
+ { date: "2026-04-14", name: "Ambedkar Jayanti" },
30
+ { date: "2026-04-17", name: "Ram Navami" },
31
+ { date: "2026-05-01", name: "Labour Day" },
32
+ { date: "2026-06-17", name: "Eid ul-Fitr" },
33
+ { date: "2026-08-15", name: "Independence Day" },
34
+ { date: "2026-08-26", name: "Raksha Bandhan" },
35
+ { date: "2026-09-05", name: "Janmashtami" },
36
+ { date: "2026-10-02", name: "Gandhi Jayanti" },
37
+ { date: "2026-10-12", name: "Dussehra" },
38
+ { date: "2026-10-20", name: "Eid ul-Adha" },
39
+ { date: "2026-10-29", name: "Diwali" },
40
+ { date: "2026-11-14", name: "Guru Nanak Jayanti" },
41
+ { date: "2026-12-25", name: "Christmas" },
42
+ ],
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Get all available country keys
48
+ * @returns {string[]} Array of country keys
49
+ */
50
+ export function getAvailableCountries() {
51
+ return Object.keys(countryPresets);
52
+ }
53
+
54
+ /**
55
+ * Get country information (name, key pairs)
56
+ * @returns {Array<{key: string, name: string}>} Array of country info objects
57
+ */
58
+ export function getCountryList() {
59
+ return Object.entries(countryPresets).map(([key, preset]) => ({
60
+ key,
61
+ name: preset.name,
62
+ }));
63
+ }
64
+
65
+ /**
66
+ * Get preset configuration for a country
67
+ * @param {string} countryKey - Key from countryPresets (e.g., 'germany', 'india')
68
+ * @param {number} customPTO - Optional custom PTO days (overrides default)
69
+ * @param {number} customYear - Optional custom year (overrides preset year)
70
+ * @returns {Object} Configuration object ready for optimizer
71
+ */
72
+ export function getPreset(countryKey, customPTO = null, customYear = null) {
73
+ const preset = countryPresets[countryKey];
74
+ if (!preset) {
75
+ throw new Error(
76
+ `Unknown country preset: ${countryKey}. Available: ${getAvailableCountries().join(
77
+ ", "
78
+ )}`
79
+ );
80
+ }
81
+
82
+ const year = customYear ?? preset.year;
83
+
84
+ const holidays = preset.holidays.filter((h) => {
85
+ const holidayYear = parseInt(h.date.split("-")[0]);
86
+ return holidayYear === year;
87
+ });
88
+
89
+ return {
90
+ numberOfDays: customPTO ?? preset.defaultPTO,
91
+ year: year,
92
+ holidays: holidays,
93
+ companyDaysOff: [],
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Check if a country preset exists
99
+ * @param {string} countryKey - Key to check
100
+ * @returns {boolean} True if preset exists
101
+ */
102
+ export function hasPreset(countryKey) {
103
+ return countryKey in countryPresets;
104
+ }
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { optimize, formatReport } from "./vacation_optimizer.js";
2
+ export {
3
+ countryPresets,
4
+ getAvailableCountries,
5
+ getCountryList,
6
+ getPreset,
7
+ hasPreset,
8
+ } from "./country-presets.js";
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@fineanmol/holiday-optimizer",
3
+ "version": "1.0.0",
4
+ "description": "Optimize vacation days using dynamic programming — maximize time off around weekends and public holidays.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "vacation_optimizer.js",
13
+ "country-presets.js",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "keywords": [
18
+ "vacation",
19
+ "pto",
20
+ "holiday",
21
+ "optimizer",
22
+ "planning",
23
+ "calendar",
24
+ "leave"
25
+ ],
26
+ "author": "fineanmol",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/fineanmol/holiday-optimizer.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/fineanmol/holiday-optimizer/issues"
34
+ },
35
+ "homepage": "https://github.com/fineanmol/holiday-optimizer#readme",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
@@ -0,0 +1,427 @@
1
+ class Day {
2
+ constructor({ date, isWeekend, isPublicHoliday, isCompanyDay }) {
3
+ this.date = date;
4
+ this.is_weekend = isWeekend;
5
+ this.is_public_holiday = isPublicHoliday;
6
+ this.is_company_day = isCompanyDay;
7
+
8
+ this.is_pto = false;
9
+ this.is_part_of_break = false;
10
+ }
11
+ }
12
+
13
+ class Break {
14
+ constructor({
15
+ start_date,
16
+ end_date,
17
+ total_days,
18
+ pto_days,
19
+ weekends,
20
+ public_holidays,
21
+ company_days,
22
+ days,
23
+ }) {
24
+ this.start_date = start_date;
25
+ this.end_date = end_date;
26
+ this.total_days = total_days;
27
+ this.pto_days = pto_days;
28
+ this.weekends = weekends;
29
+ this.public_holidays = public_holidays;
30
+ this.company_days = company_days;
31
+ this.days = days;
32
+ }
33
+ }
34
+
35
+ class Stats {
36
+ constructor({
37
+ total_days_off,
38
+ total_paid_leave,
39
+ total_public_holidays,
40
+ total_weekends,
41
+ total_company_days,
42
+ }) {
43
+ this.total_days_off = total_days_off;
44
+ this.total_paid_leave = total_paid_leave;
45
+ this.total_public_holidays = total_public_holidays;
46
+ this.total_weekends = total_weekends;
47
+ this.total_company_days = total_company_days;
48
+ }
49
+ }
50
+
51
+ class Result {
52
+ constructor({ days, breaks, stats }) {
53
+ this.days = days;
54
+ this.breaks = breaks;
55
+ this.stats = stats;
56
+ }
57
+ }
58
+
59
+ function pad2(n) {
60
+ return String(n).padStart(2, "0");
61
+ }
62
+
63
+ function formatDate(d) {
64
+ const y = d.getFullYear();
65
+ const m = pad2(d.getMonth() + 1);
66
+ const day = pad2(d.getDate());
67
+ return `${y}-${m}-${day}`;
68
+ }
69
+
70
+ function parseISO(iso) {
71
+ const [y, m, d] = iso.split("-").map(Number);
72
+ return new Date(y, m - 1, d);
73
+ }
74
+
75
+ function addDays(d, n) {
76
+ const copy = new Date(d.getTime());
77
+ copy.setDate(copy.getDate() + n);
78
+ return copy;
79
+ }
80
+
81
+ function compareISO(a, b) {
82
+ if (a < b) return -1;
83
+ if (a > b) return 1;
84
+ return 0;
85
+ }
86
+
87
+ function isFixedOff(day) {
88
+ return day.is_weekend || day.is_public_holiday || day.is_company_day;
89
+ }
90
+
91
+ function buildCalendar(params) {
92
+ const yr = params.year ?? new Date().getFullYear();
93
+ const today = new Date();
94
+
95
+ let start;
96
+ if (params.startDate) {
97
+ start = parseISO(params.startDate);
98
+ } else {
99
+ start =
100
+ yr === today.getFullYear()
101
+ ? new Date(today.getFullYear(), today.getMonth(), today.getDate())
102
+ : new Date(yr, 0, 1);
103
+ }
104
+ const end = new Date(yr, 11, 31);
105
+
106
+ const holidays = new Set((params.holidays ?? []).map((h) => h.date));
107
+ const company = new Set((params.companyDaysOff ?? []).map((c) => c.date));
108
+
109
+ const days = [];
110
+ let d = start;
111
+
112
+ while (d <= end) {
113
+ const ds = formatDate(d);
114
+ const weekday = d.getDay();
115
+ const isWeekend = weekday === 0 || weekday === 6;
116
+
117
+ days.push(
118
+ new Day({
119
+ date: ds,
120
+ isWeekend,
121
+ isPublicHoliday: holidays.has(ds),
122
+ isCompanyDay: company.has(ds),
123
+ })
124
+ );
125
+
126
+ d = addDays(d, 1);
127
+ }
128
+
129
+ return days;
130
+ }
131
+
132
+ function generateCandidates(cal, minLen, maxLen) {
133
+ const candidates = [];
134
+ const n = cal.length;
135
+
136
+ for (let i = 0; i < n; i++) {
137
+ for (let L = minLen; L <= maxLen; L++) {
138
+ if (i + L > n) break;
139
+ const seg = cal.slice(i, i + L);
140
+ const ptoUsed = seg.reduce((acc, d) => acc + (isFixedOff(d) ? 0 : 1), 0);
141
+ if (ptoUsed === 0) continue;
142
+
143
+ candidates.push({
144
+ start_idx: i,
145
+ end_idx: i + L - 1,
146
+ total_days: L,
147
+ pto_used: ptoUsed,
148
+ eff: L / ptoUsed,
149
+ segment: seg,
150
+ });
151
+ }
152
+ }
153
+
154
+ return candidates;
155
+ }
156
+
157
+ function pruneCandidates(cands, maxPTO) {
158
+ cands = cands.filter((c) => c.pto_used <= maxPTO);
159
+
160
+ const byStart = new Map();
161
+ for (const c of cands) {
162
+ if (!byStart.has(c.start_idx)) byStart.set(c.start_idx, []);
163
+ byStart.get(c.start_idx).push(c);
164
+ }
165
+
166
+ const pruned = [];
167
+ for (const [start, items] of byStart.entries()) {
168
+ items.sort(
169
+ (a, b) => a.pto_used - b.pto_used || b.total_days - a.total_days
170
+ );
171
+
172
+ const best = [];
173
+ for (const cand of items) {
174
+ const dominated = best.some(
175
+ (b) =>
176
+ b.end_idx >= cand.end_idx &&
177
+ b.pto_used <= cand.pto_used &&
178
+ b.total_days >= cand.total_days
179
+ );
180
+ if (!dominated) best.push(cand);
181
+ }
182
+ pruned.push(...best);
183
+ }
184
+
185
+ pruned.sort((a, b) => a.start_idx - b.start_idx);
186
+ return pruned;
187
+ }
188
+
189
+ function binarySearchNext(cands, startPos) {
190
+ let lo = 0;
191
+ let hi = cands.length;
192
+ while (lo < hi) {
193
+ const mid = Math.floor((lo + hi) / 2);
194
+ if (cands[mid].start_idx < startPos) lo = mid + 1;
195
+ else hi = mid;
196
+ }
197
+ return lo;
198
+ }
199
+
200
+ function dpSelect(cands, maxPTO, spacing) {
201
+ if (!cands.length || maxPTO <= 0) return [];
202
+
203
+ const nextIndices = cands.map((c) =>
204
+ binarySearchNext(cands, c.end_idx + 1 + spacing)
205
+ );
206
+ const n = cands.length;
207
+
208
+ const dpDays = Array.from({ length: n + 1 }, () => Array(maxPTO + 1).fill(0));
209
+ const dpChoice = Array.from({ length: n + 1 }, () =>
210
+ Array.from({ length: maxPTO + 1 }, () => [])
211
+ );
212
+
213
+ for (let idx = n - 1; idx >= 0; idx--) {
214
+ const cand = cands[idx];
215
+ const cost = cand.pto_used;
216
+ const totalDays = cand.total_days;
217
+ const jump = nextIndices[idx];
218
+
219
+ for (let p = 0; p <= maxPTO; p++) {
220
+ let bestDays = dpDays[idx + 1][p];
221
+ let bestChoice = dpChoice[idx + 1][p];
222
+
223
+ if (cost <= p) {
224
+ const takeDays = totalDays + dpDays[jump][p - cost];
225
+ if (takeDays > bestDays) {
226
+ bestDays = takeDays;
227
+ bestChoice = [idx, ...dpChoice[jump][p - cost]];
228
+ }
229
+ }
230
+
231
+ dpDays[idx][p] = bestDays;
232
+ dpChoice[idx][p] = bestChoice;
233
+ }
234
+ }
235
+
236
+ const choice = dpChoice[0][maxPTO];
237
+ return choice.map((i) => cands[i]);
238
+ }
239
+
240
+ function forceExtend(cal, breaks, remaining) {
241
+ if (remaining <= 0) return remaining;
242
+
243
+ for (const br of breaks) {
244
+ const endDate = parseISO(br.end_date);
245
+ const nextDay = addDays(endDate, 1);
246
+ const nextISO = formatDate(nextDay);
247
+
248
+ const idx = cal.findIndex((d) => d.date === nextISO);
249
+ if (
250
+ idx !== -1 &&
251
+ remaining > 0 &&
252
+ !cal[idx].is_part_of_break &&
253
+ !isFixedOff(cal[idx])
254
+ ) {
255
+ cal[idx].is_part_of_break = true;
256
+ cal[idx].is_pto = true;
257
+
258
+ br.days.push(cal[idx]);
259
+ br.end_date = cal[idx].date;
260
+ br.total_days += 1;
261
+ br.pto_days += 1;
262
+ remaining -= 1;
263
+ }
264
+ }
265
+ return remaining;
266
+ }
267
+
268
+ function addForcedSegments(cal, remaining) {
269
+ const forced = [];
270
+ let i = 0;
271
+ const n = cal.length;
272
+
273
+ while (i < n && remaining > 0) {
274
+ if (cal[i].is_part_of_break || isFixedOff(cal[i])) {
275
+ i++;
276
+ continue;
277
+ }
278
+
279
+ const seg = [];
280
+ while (
281
+ i < n &&
282
+ remaining > 0 &&
283
+ !cal[i].is_part_of_break &&
284
+ !isFixedOff(cal[i])
285
+ ) {
286
+ cal[i].is_part_of_break = true;
287
+ cal[i].is_pto = true;
288
+ seg.push(cal[i]);
289
+ remaining--;
290
+ i++;
291
+ }
292
+
293
+ if (seg.length) {
294
+ forced.push(
295
+ new Break({
296
+ start_date: seg[0].date,
297
+ end_date: seg[seg.length - 1].date,
298
+ total_days: seg.length,
299
+ pto_days: seg.length,
300
+ weekends: seg.filter((d) => d.is_weekend).length,
301
+ public_holidays: seg.filter((d) => d.is_public_holiday).length,
302
+ company_days: seg.filter((d) => d.is_company_day).length,
303
+ days: seg,
304
+ })
305
+ );
306
+ }
307
+
308
+ i++;
309
+ }
310
+
311
+ return forced;
312
+ }
313
+
314
+ function optimize(params) {
315
+ const maxPTO = Number(params.numberOfDays);
316
+ const minLen = params.minBreak ?? 4;
317
+ const maxLen = params.maxBreak ?? 9;
318
+ const spacing = params.timeBetweenBreaks ?? 21;
319
+
320
+ const cal = buildCalendar(params);
321
+ let candidates = generateCandidates(cal, minLen, maxLen);
322
+ candidates = pruneCandidates(candidates, maxPTO);
323
+
324
+ const chosen = dpSelect(candidates, maxPTO, spacing);
325
+
326
+ const breaks = [];
327
+ for (const seg of chosen) {
328
+ for (let idx = seg.start_idx; idx <= seg.end_idx; idx++) {
329
+ cal[idx].is_part_of_break = true;
330
+ if (!isFixedOff(cal[idx])) cal[idx].is_pto = true;
331
+ }
332
+
333
+ const segmentDays = seg.segment;
334
+ breaks.push(
335
+ new Break({
336
+ start_date: segmentDays[0].date,
337
+ end_date: segmentDays[segmentDays.length - 1].date,
338
+ total_days: seg.total_days,
339
+ pto_days: seg.pto_used,
340
+ weekends: segmentDays.filter((d) => d.is_weekend).length,
341
+ public_holidays: segmentDays.filter((d) => d.is_public_holiday).length,
342
+ company_days: segmentDays.filter((d) => d.is_company_day).length,
343
+ days: [...segmentDays],
344
+ })
345
+ );
346
+ }
347
+
348
+ let usedPTO = breaks.reduce((acc, b) => acc + b.pto_days, 0);
349
+ let remaining = maxPTO - usedPTO;
350
+
351
+ let prevRemaining = remaining + 1;
352
+ while (remaining > 0 && remaining < prevRemaining) {
353
+ prevRemaining = remaining;
354
+ remaining = forceExtend(cal, breaks, remaining);
355
+
356
+ const extra = addForcedSegments(cal, remaining);
357
+ breaks.push(...extra);
358
+
359
+ usedPTO = breaks.reduce((acc, b) => acc + b.pto_days, 0);
360
+ remaining = maxPTO - usedPTO;
361
+ }
362
+
363
+ const stats = new Stats({
364
+ total_days_off: breaks.reduce((acc, b) => acc + b.total_days, 0),
365
+ total_paid_leave: breaks.reduce((acc, b) => acc + b.pto_days, 0),
366
+ total_public_holidays: breaks.reduce(
367
+ (acc, b) => acc + b.public_holidays,
368
+ 0
369
+ ),
370
+ total_weekends: breaks.reduce((acc, b) => acc + b.weekends, 0),
371
+ total_company_days: breaks.reduce((acc, b) => acc + b.company_days, 0),
372
+ });
373
+
374
+ return new Result({ days: cal, breaks, stats });
375
+ }
376
+
377
+ function formatReport(res, params) {
378
+ const lines = [];
379
+ lines.push("Holiday Optimizer Report (JavaScript)");
380
+ lines.push("=====================================");
381
+ lines.push(`Year: ${params.year ?? new Date().getFullYear()}`);
382
+ lines.push(`Requested Paid Leave Days: ${params.numberOfDays}`);
383
+ lines.push("");
384
+
385
+ lines.push("Summary");
386
+ lines.push("-------");
387
+ lines.push(`Total Days Off: ${res.stats.total_days_off}`);
388
+ lines.push(`Total Paid Leave Used: ${res.stats.total_paid_leave}`);
389
+ lines.push(`Public Holidays in Breaks: ${res.stats.total_public_holidays}`);
390
+ lines.push(`Weekends in Breaks: ${res.stats.total_weekends}`);
391
+ if (res.stats.total_company_days > 0) {
392
+ lines.push(`Company Days in Breaks: ${res.stats.total_company_days}`);
393
+ }
394
+ lines.push("");
395
+
396
+ lines.push("Breaks");
397
+ lines.push("------");
398
+ if (!res.breaks.length) {
399
+ lines.push("No breaks were scheduled.");
400
+ } else {
401
+ res.breaks.forEach((br, idx) => {
402
+ const companyPart = br.company_days
403
+ ? ` | Company ${br.company_days}`
404
+ : "";
405
+ lines.push(`Break ${idx + 1}: ${br.start_date} → ${br.end_date}`);
406
+ lines.push(
407
+ ` • Total ${br.total_days} days | Paid Leave ${br.pto_days} | ` +
408
+ `Weekends ${br.weekends} | Public ${br.public_holidays}${companyPart}`
409
+ );
410
+
411
+ const ptoDates = br.days.filter((d) => d.is_pto).map((d) => d.date);
412
+ // if (ptoDates.length) {
413
+ // lines.push(` • Paid leave dates: ${ptoDates.join(", ")}`);
414
+ // }
415
+ lines.push("");
416
+ });
417
+ }
418
+
419
+ const ptoFlat = res.days.filter((d) => d.is_pto).map((d) => d.date);
420
+ lines.push("Paid Leave Dates (all)");
421
+ lines.push("----------------------");
422
+ lines.push(ptoFlat.length ? ptoFlat.join(", ") : "None");
423
+
424
+ return lines.join("\n");
425
+ }
426
+
427
+ export { optimize, formatReport };