@happychef/algorithm 1.2.10 → 1.2.11

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/assignTables.js CHANGED
@@ -59,7 +59,7 @@ function isTemporaryTableValid(table, reservationDateStr, reservationTimeStr) {
59
59
  if (!table.startDate || !table.endDate) {
60
60
  return false; // Invalid temporary table definition
61
61
  }
62
-
62
+
63
63
  // Basic date string comparison (YYYY-MM-DD format)
64
64
  if (reservationDateStr < table.startDate || reservationDateStr > table.endDate) {
65
65
  return false;
@@ -70,7 +70,7 @@ function isTemporaryTableValid(table, reservationDateStr, reservationTimeStr) {
70
70
  if (!reservationMealType) {
71
71
  return false; // Cannot determine meal type for the reservation
72
72
  }
73
-
73
+
74
74
  if (table.application !== reservationMealType) {
75
75
  return false;
76
76
  }
@@ -89,7 +89,7 @@ function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
89
89
  if (!timePattern.test(timeString)) {
90
90
  throw new Error("Invalid time format. Expected 'HH:MM'.");
91
91
  }
92
-
92
+
93
93
  const [hour, minute] = timeString.split(":").map(Number);
94
94
  const startMinutes = hour * 60 + minute;
95
95
  const slotCount = Math.ceil(durationMinutes / intervalMinutes);
@@ -99,7 +99,7 @@ function computeRequiredSlots(timeString, durationMinutes, intervalMinutes) {
99
99
  }
100
100
  return slots;
101
101
  }
102
-
102
+
103
103
  /**
104
104
  * isTableFreeForAllSlots(tableNumber, requiredSlots, tableOccupiedSlots)
105
105
  * Checks if the given tableNumber is free (no overlapping) for all requiredSlots.
@@ -109,7 +109,7 @@ function isTableFreeForAllSlots(tableNumber, requiredSlots, tableOccupiedSlots)
109
109
  // If any slot from requiredSlots is in occupiedSlots, the table is not free
110
110
  return !requiredSlots.some(slot => occupiedSlots.has(slot));
111
111
  }
112
-
112
+
113
113
  /**
114
114
  * calculateDistance(tableA, tableB)
115
115
  * Euclidean distance between two tables (for multi-table distance minimization).
@@ -119,7 +119,7 @@ function calculateDistance(tableA, tableB) {
119
119
  const dy = tableA.y - tableB.y;
120
120
  return Math.sqrt(dx * dx + dy * dy);
121
121
  }
122
-
122
+
123
123
  function distanceSum(set) {
124
124
  let sum = 0;
125
125
  for (let i = 0; i < set.length; i++) {
@@ -196,7 +196,20 @@ function findMultiTableCombination(
196
196
  }
197
197
  }
198
198
 
199
-
199
+ /**
200
+ * Gets the floor ID linked to a seat place from seatAreaFloorLinks.
201
+ * @param {Object} restaurantSettings - The restaurant settings object.
202
+ * @param {string} seatPlace - The seat place identifier.
203
+ * @returns {string|null} The floor ID or null if not found.
204
+ */
205
+ function getFloorIdForSeatPlace(restaurantSettings, seatPlace) {
206
+ if (!seatPlace || !restaurantSettings) return null;
207
+ const seatAreaFloorLinks = restaurantSettings["general-settings"]?.seatAreaFloorLinks;
208
+ if (!seatAreaFloorLinks || typeof seatAreaFloorLinks !== 'object') return null;
209
+ return seatAreaFloorLinks[seatPlace] || null;
210
+ }
211
+
212
+
200
213
  /**
201
214
  * assignTablesIfPossible
202
215
  * Attempts to assign tables to a reservation based on availability and constraints.
@@ -211,7 +224,8 @@ async function assignTablesIfPossible({
211
224
  const date = reservation.date;
212
225
  const time = reservation.time;
213
226
  const guests = reservation.guests;
214
-
227
+ const zitplaats = reservation.zitplaats;
228
+
215
229
  // 1) First, fetch restaurant data (which contains the floors information)
216
230
  const restaurantSettings = await db.collection('restaurants').findOne({ _id: restaurantId });
217
231
  if (!restaurantSettings) {
@@ -221,9 +235,9 @@ async function assignTablesIfPossible({
221
235
  return; // Non-enforcing: skip table assignment
222
236
  }
223
237
  }
224
-
238
+
225
239
  // 2) Get floors data directly from the restaurant document
226
- const floorsData = restaurantSettings.floors;
240
+ let floorsData = restaurantSettings.floors;
227
241
  if (!floorsData || !Array.isArray(floorsData) || floorsData.length === 0) {
228
242
  if (enforceTableAvailability) {
229
243
  throw new Error('No floors data found in restaurant document.');
@@ -231,31 +245,51 @@ async function assignTablesIfPossible({
231
245
  return; // Non-enforcing: skip table assignment
232
246
  }
233
247
  }
234
-
235
- // 3) Collect all "Tafel" tables
236
- let allTables = [];
237
- floorsData.forEach(floor => {
238
- if (floor.tables && Array.isArray(floor.tables)) {
239
- floor.tables.forEach(tbl => {
240
- if (tbl.objectType === "Tafel") {
241
- allTables.push({
242
- tableId: tbl.id,
243
- tableNumber: parseInt(tbl.tableNumber.$numberInt || tbl.tableNumber),
244
- minCapacity: parseInt(tbl.minCapacity.$numberInt || tbl.minCapacity),
245
- maxCapacity: parseInt(tbl.maxCapacity.$numberInt || tbl.maxCapacity),
246
- priority: parseInt(tbl.priority.$numberInt || tbl.priority),
247
- x: parseInt(tbl.x.$numberInt || tbl.x),
248
- y: parseInt(tbl.y.$numberInt || tbl.y),
249
- isTemporary: tbl.isTemporary === true, // Added for temporary tables
250
- startDate: tbl.startDate || null, // Added for temporary tables
251
- endDate: tbl.endDate || null, // Added for temporary tables
252
- application: tbl.application || null // Added for temporary tables
253
- });
254
- }
255
- });
248
+
249
+ // 2.5) If zitplaats is specified and has a floor link, try that floor first
250
+ const linkedFloorId = getFloorIdForSeatPlace(restaurantSettings, zitplaats);
251
+ let preferredFloorOnly = null;
252
+ if (linkedFloorId) {
253
+ const linkedFloor = floorsData.find(f => f.id === linkedFloorId);
254
+ if (linkedFloor) {
255
+ preferredFloorOnly = [linkedFloor];
256
+ console.log(`[Zitplaats] Will try floor '${linkedFloorId}' first for zitplaats '${zitplaats}'`);
256
257
  }
257
- });
258
-
258
+ }
259
+
260
+ // 3) Helper to collect tables from floors
261
+ function collectTablesFromFloors(floors) {
262
+ const tables = [];
263
+ floors.forEach(floor => {
264
+ if (floor.tables && Array.isArray(floor.tables)) {
265
+ floor.tables.forEach(tbl => {
266
+ if (tbl.objectType === "Tafel") {
267
+ tables.push({
268
+ tableId: tbl.id,
269
+ tableNumber: parseInt(tbl.tableNumber.$numberInt || tbl.tableNumber),
270
+ minCapacity: parseInt(tbl.minCapacity.$numberInt || tbl.minCapacity),
271
+ maxCapacity: parseInt(tbl.maxCapacity.$numberInt || tbl.maxCapacity),
272
+ priority: parseInt(tbl.priority.$numberInt || tbl.priority),
273
+ x: parseInt(tbl.x.$numberInt || tbl.x),
274
+ y: parseInt(tbl.y.$numberInt || tbl.y),
275
+ isTemporary: tbl.isTemporary === true,
276
+ startDate: tbl.startDate || null,
277
+ endDate: tbl.endDate || null,
278
+ application: tbl.application || null
279
+ });
280
+ }
281
+ });
282
+ }
283
+ });
284
+ return tables;
285
+ }
286
+
287
+ // Collect all tables from all floors
288
+ let allTables = collectTablesFromFloors(floorsData);
289
+
290
+ // Collect tables from preferred floor only (if specified)
291
+ let preferredFloorTables = preferredFloorOnly ? collectTablesFromFloors(preferredFloorOnly) : null;
292
+
259
293
  // 4) If no tables found
260
294
  if (!allTables.length) {
261
295
  if (enforceTableAvailability) {
@@ -264,44 +298,33 @@ async function assignTablesIfPossible({
264
298
  return; // Non-enforcing: skip table assignment
265
299
  }
266
300
  }
267
-
268
- // 5) Sort the tables
269
- allTables.sort((a, b) => {
270
- if (a.maxCapacity !== b.maxCapacity) {
271
- return a.maxCapacity - b.maxCapacity;
272
- }
273
- if (a.priority !== b.priority) {
274
- return a.priority - b.priority;
275
- }
276
- return a.minCapacity - b.minCapacity;
277
- });
278
-
279
- // 6) Get duration and interval settings
301
+
302
+ // 5) Get duration and interval settings
280
303
  const duurReservatie = parseInt(
281
304
  restaurantSettings["general-settings"]?.duurReservatie?.$numberInt || restaurantSettings["general-settings"]?.duurReservatie || 120
282
305
  );
283
306
  const intervalReservatie = parseInt(
284
307
  restaurantSettings["general-settings"]?.intervalReservatie?.$numberInt || restaurantSettings["general-settings"]?.intervalReservatie || 30
285
308
  );
286
-
309
+
287
310
  // 7) Compute the requiredSlots for this reservation
288
311
  const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
289
-
312
+
290
313
  // 8) Get overlapping reservations (same date, same restaurant)
291
314
  const overlappingReservations = await db.collection('reservations').find({
292
315
  restaurantId: restaurantId,
293
316
  date: date
294
317
  }).toArray();
295
-
318
+
296
319
  // 9) Build tableOccupiedSlots map
297
320
  let tableOccupiedSlots = {}; // { [tableNumber]: Set([...slots]) }
298
321
  for (let r of overlappingReservations) {
299
322
  // No need to skip the current reservation as it's not yet inserted
300
-
323
+
301
324
  // compute that reservation's time slots
302
325
  const rDuration = duurReservatie; // assuming same duration
303
326
  const rSlots = computeRequiredSlots(r.time, rDuration, intervalReservatie);
304
-
327
+
305
328
  if (r.tables) {
306
329
  for (let tn of r.tables) {
307
330
  if (!tableOccupiedSlots[tn]) {
@@ -313,86 +336,89 @@ async function assignTablesIfPossible({
313
336
  }
314
337
  }
315
338
  }
316
- // ===== 9.5) Table Grouping Attempt (moved to grouping.js) =====
317
- try {
318
- const groupingResult = tryGroupTables({
319
- restaurantSettings,
320
- allTables,
321
- guests,
322
- date,
323
- time,
324
- requiredSlots,
325
- tableOccupiedSlots,
326
- isTemporaryTableValid,
327
- isTableFreeForAllSlots,
339
+
340
+ // 10) Helper function to try table assignment from a given set of tables
341
+ function tryAssignTables(tables, label) {
342
+ // Sort tables
343
+ const sortedTables = [...tables].sort((a, b) => {
344
+ if (a.maxCapacity !== b.maxCapacity) return a.maxCapacity - b.maxCapacity;
345
+ if (a.priority !== b.priority) return a.priority - b.priority;
346
+ return a.minCapacity - b.minCapacity;
328
347
  });
329
348
 
330
- if (groupingResult) {
331
- reservation.tables = groupingResult.tables;
332
- reservation.tableIds = groupingResult.tableIds;
333
- reservation._viaGroup = groupingResult.viaGroup;
334
- console.log(
335
- `[Grouping] via '${reservation._viaGroup}' -> tables ${reservation.tables.join(",")}`
336
- );
337
- return; // grouping wins
349
+ // Try grouping first
350
+ try {
351
+ const groupingResult = tryGroupTables({
352
+ restaurantSettings,
353
+ allTables: sortedTables,
354
+ guests,
355
+ date,
356
+ time,
357
+ requiredSlots,
358
+ tableOccupiedSlots,
359
+ isTemporaryTableValid,
360
+ isTableFreeForAllSlots,
361
+ });
362
+
363
+ if (groupingResult) {
364
+ reservation.tables = groupingResult.tables;
365
+ reservation.tableIds = groupingResult.tableIds;
366
+ reservation._viaGroup = groupingResult.viaGroup;
367
+ console.log(`[${label}] [Grouping] via '${reservation._viaGroup}' -> tables ${reservation.tables.join(",")}`);
368
+ return true;
369
+ }
370
+ } catch (err) {
371
+ throw err;
338
372
  }
339
- } catch (err) {
340
- // strict grouping throws; preserve original behavior
341
- throw err;
342
- }
343
- // ===== end 9.5) Table Grouping Attempt =====
344
373
 
345
- // 10) Single-Table Attempt
346
- for (let t of allTables) {
347
- // Check if temporary table is valid for this date/time
348
- if (!isTemporaryTableValid(t, date, time)) {
349
- continue; // Skip invalid temporary table
374
+ // Try single-table assignment
375
+ for (let t of sortedTables) {
376
+ if (!isTemporaryTableValid(t, date, time)) continue;
377
+ if (t.minCapacity <= guests && guests <= t.maxCapacity &&
378
+ isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)) {
379
+ reservation.tables = [t.tableNumber];
380
+ reservation.tableIds = [t.tableId];
381
+ console.log(`[${label}] Assigned to table: ${t.tableNumber}`);
382
+ return true;
383
+ }
350
384
  }
351
-
352
- if (
353
- t.minCapacity <= guests &&
354
- guests <= t.maxCapacity &&
355
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
356
- ) {
357
- // Assign this table to the reservation
358
- reservation.tables = [t.tableNumber];
359
- reservation.tableIds = [t.tableId];
360
- console.log(`Reservation assigned to table: ${t.tableNumber}`);
361
- return; // Assignment successful
385
+
386
+ // Try multi-table combination
387
+ let best = { minDistance: Infinity, tables: [] };
388
+ findMultiTableCombination(sortedTables, guests, 0, [], best, requiredSlots, tableOccupiedSlots, date, time);
389
+
390
+ if (best.tables.length > 0) {
391
+ reservation.tables = best.tables.map(t => t.tableNumber);
392
+ reservation.tableIds = best.tables.map(t => t.tableId);
393
+ console.log(`[${label}] Assigned to tables: ${reservation.tables.join(', ')}`);
394
+ return true;
362
395
  }
396
+
397
+ return false;
363
398
  }
364
-
365
- // 11) Multi-Table Attempt
366
- let best = { minDistance: Infinity, tables: [] };
367
-
368
- findMultiTableCombination(
369
- allTables,
370
- guests,
371
- 0,
372
- [],
373
- best,
374
- requiredSlots,
375
- tableOccupiedSlots,
376
- date, // Pass date for temporary table validation
377
- time // Pass time for temporary table validation
378
- );
379
-
380
- if (best.tables.length > 0) {
381
- // Assign the best combination to the reservation
382
- reservation.tables = best.tables.map(t => t.tableNumber);
383
- reservation.tableIds = best.tables.map(t => t.tableId);
384
- console.log(`Reservation assigned to tables: ${reservation.tables.join(', ')}`);
385
- return; // Assignment successful
386
- } else {
387
- // If no valid table combo found, either fail (if enforce) or do nothing (if not enforce)
388
- if (enforceTableAvailability) {
389
- throw new Error('Unable to find enough tables for this reservation with enforcement on.');
390
- } else {
391
- console.log('No tables available, but non-enforcing mode => continuing without assignment.');
399
+
400
+ // 11) Try preferred floor first (if specified), then fall back to all floors
401
+ if (preferredFloorTables && preferredFloorTables.length > 0) {
402
+ console.log(`[Zitplaats] Trying preferred floor first (${preferredFloorTables.length} tables)`);
403
+ if (tryAssignTables(preferredFloorTables, 'PreferredFloor')) {
404
+ return; // Success on preferred floor
392
405
  }
406
+ console.log(`[Zitplaats] No availability on preferred floor, falling back to all floors`);
407
+ }
408
+
409
+ // 12) Try all floors
410
+ if (tryAssignTables(allTables, 'AllFloors')) {
411
+ return; // Success
412
+ }
413
+
414
+ // 13) No valid table combo found
415
+ if (enforceTableAvailability) {
416
+ throw new Error('Unable to find enough tables for this reservation with enforcement on.');
417
+ } else {
418
+ console.log('No tables available, but non-enforcing mode => continuing without assignment.');
393
419
  }
394
420
  }
395
-
421
+
396
422
  module.exports = {
397
423
  assignTablesIfPossible
398
- };
424
+ };
@@ -0,0 +1,16 @@
1
+ # PR 4 - TEST: Add intentionally failing tests
2
+
3
+ **Actions:**
4
+
5
+ ## Changes Summary
6
+ NO_ADDITIONS
7
+
8
+ NO_REMOVALS
9
+
10
+ NO_CHANGES
11
+
12
+ ---
13
+
14
+ **Author:** houssammk123
15
+ **Date:** 2025-12-09
16
+ **PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/4
@@ -0,0 +1,16 @@
1
+ # PR 5 - TEST: Add passing helper tests
2
+
3
+ **Actions:**
4
+
5
+ ## Changes Summary
6
+ NO_ADDITIONS
7
+
8
+ NO_REMOVALS
9
+
10
+ NO_CHANGES
11
+
12
+ ---
13
+
14
+ **Author:** houssammk123
15
+ **Date:** 2025-12-09
16
+ **PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/5
@@ -0,0 +1,18 @@
1
+ # PR 6 - Fix remove meaningless tests
2
+
3
+ **Actions:**
4
+
5
+ ## Changes Summary
6
+ NO_ADDITIONS
7
+
8
+ REMOVED:
9
+ - Removed test case `test_remove_meaningless_tests` from the test suite.
10
+ - Removed the entire test file `test_remove_meaningless_tests.py` from the repository.
11
+
12
+ NO_CHANGES
13
+
14
+ ---
15
+
16
+ **Author:** houssammk123
17
+ **Date:** 2025-12-09
18
+ **PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/6
@@ -0,0 +1,22 @@
1
+ # PR 7 - Fix/infinite loop table assignment
2
+
3
+ **Actions:**
4
+
5
+ ## Changes Summary
6
+ ADDED:
7
+ - New function `findTableForParty(tables, partySize)` added to handle table assignment logic.
8
+ - New variable `assignedTables` introduced to track which tables have been assigned.
9
+ - New import `itertools` added to support combination generation for table selection.
10
+
11
+ NO_REMOVALS
12
+
13
+ CHANGED:
14
+ - Modified the `assignTables` function in `src/tableAssignment.js` to replace the `while` loop with a `for` loop that iterates over a pre-calculated `availableTables` array to prevent infinite loops.
15
+ - Updated the table selection logic inside the loop from a random index selection (`Math.floor(Math.random() * availableTables.length)`) to a sequential index selection using the loop counter `i`.
16
+ - Changed the condition for breaking out of the assignment loop from checking `remainingGuests > 0` to checking if `availableTables.length > 0` and if the current table's capacity is sufficient for the party size.
17
+
18
+ ---
19
+
20
+ **Author:** houssammk123
21
+ **Date:** 2025-12-12
22
+ **PR Link:** https://github.com/thibaultvandesompele2/15-happy-algorithm/pull/7
package/jest.config.js CHANGED
@@ -1,23 +1,23 @@
1
- module.exports = {
2
- testEnvironment: 'node',
3
- coverageDirectory: 'coverage',
4
- collectCoverageFrom: [
5
- '**/*.js',
6
- '!**/node_modules/**',
7
- '!**/coverage/**',
8
- '!jest.config.js',
9
- '!test.js',
10
- '!test-*.js',
11
- '!**/__tests__/**'
12
- ],
13
- testMatch: [
14
- '**/__tests__/**/*.test.js'
15
- ],
16
- testPathIgnorePatterns: [
17
- '/node_modules/',
18
- '/test\\.js$',
19
- '/test-.*\\.js$'
20
- ],
21
- verbose: true,
22
- testTimeout: 10000
23
- };
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ coverageDirectory: 'coverage',
4
+ collectCoverageFrom: [
5
+ '**/*.js',
6
+ '!**/node_modules/**',
7
+ '!**/coverage/**',
8
+ '!jest.config.js',
9
+ '!test.js',
10
+ '!test-*.js',
11
+ '!**/__tests__/**'
12
+ ],
13
+ testMatch: [
14
+ '**/__tests__/**/*.test.js'
15
+ ],
16
+ testPathIgnorePatterns: [
17
+ '/node_modules/',
18
+ '/test\\.js$',
19
+ '/test-.*\\.js$'
20
+ ],
21
+ verbose: true,
22
+ testTimeout: 10000
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happychef/algorithm",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "Restaurant and reservation algorithm utilities",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tableHelpers.js CHANGED
@@ -171,8 +171,8 @@ function isTemporaryTableValid(table, reservationDateStr, reservationTimeStr) {
171
171
  // Use CommonJS exports (adjust if using ES6 modules)
172
172
  module.exports = {
173
173
  shifts,
174
- parseTime, // Export if needed elsewhere, otherwise could be kept internal
174
+ parseTime,
175
175
  getMealTypeByTime,
176
176
  getAllTables,
177
177
  isTemporaryTableValid
178
- };
178
+ };