@happychef/algorithm 1.1.0 → 1.1.2
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/package.json +1 -1
- package/simulateTableAssignment.js +112 -100
package/package.json
CHANGED
|
@@ -135,10 +135,15 @@ function calculateDistance(tableA, tableB) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
138
|
+
* HIGHLY OPTIMIZED: Backtracking with aggressive pruning for multi-table assignment.
|
|
139
|
+
* Key optimizations:
|
|
140
|
+
* - Tables are PRE-FILTERED, so no validity checks needed here
|
|
141
|
+
* - Aggressive branch pruning with capacity tracking
|
|
142
|
+
* - Early termination on optimal solutions
|
|
143
|
+
* - Minimal logging to reduce overhead
|
|
139
144
|
*/
|
|
140
|
-
function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best
|
|
141
|
-
// Base case: All guests
|
|
145
|
+
function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best) {
|
|
146
|
+
// Base case: All guests seated
|
|
142
147
|
if (guestsNeeded <= 0) {
|
|
143
148
|
// Calculate total distance for the current combination
|
|
144
149
|
let distanceSum = 0;
|
|
@@ -147,105 +152,87 @@ function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet,
|
|
|
147
152
|
distanceSum += calculateDistance(currentSet[i], currentSet[j]);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
|
-
// Update
|
|
155
|
+
// Update if better (fewer tables, or same count with lower distance)
|
|
151
156
|
if (currentSet.length < best.tableCount || (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
152
157
|
best.minDistance = distanceSum;
|
|
153
|
-
best.tables = [...currentSet];
|
|
158
|
+
best.tables = [...currentSet];
|
|
154
159
|
best.tableCount = currentSet.length;
|
|
155
|
-
console.log(`Found new best table combination: ${best.tables.map(t => t.tableNumber).join(', ')} for ${guestsNeeded} guests`);
|
|
156
160
|
}
|
|
157
161
|
return;
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
// PRUNING: Can't improve on current best table count
|
|
165
|
+
if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) {
|
|
161
166
|
return;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
// PRUNING: Not enough tables remaining
|
|
170
|
+
if (startIndex >= tables.length) {
|
|
171
|
+
return;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
// PRUNING: Calculate remaining capacity (tables are pre-filtered, so all are valid)
|
|
175
|
+
let maxRemainingCapacity = 0;
|
|
169
176
|
for (let i = startIndex; i < tables.length; i++) {
|
|
170
|
-
|
|
171
|
-
// Only consider valid & free tables for potential capacity
|
|
172
|
-
if (isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr) &&
|
|
173
|
-
isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
174
|
-
{
|
|
175
|
-
maxPossibleCapacity += tbl.maxCapacity;
|
|
176
|
-
}
|
|
177
|
+
maxRemainingCapacity += tables[i].maxCapacity;
|
|
177
178
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return; // Impossible to seat remaining guests
|
|
179
|
+
if (maxRemainingCapacity < guestsNeeded) {
|
|
180
|
+
return; // Impossible to satisfy
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// OPTIMIZATION: Limit search depth for large table sets
|
|
184
|
+
const remainingTables = tables.length - startIndex;
|
|
185
|
+
if (remainingTables > 15 && currentSet.length === 0) {
|
|
186
|
+
// For first iteration with many tables, only try largest tables
|
|
187
|
+
const sortedBySize = tables.slice(startIndex).sort((a, b) => b.maxCapacity - a.maxCapacity);
|
|
188
|
+
const topTables = sortedBySize.slice(0, Math.min(10, sortedBySize.length));
|
|
189
|
+
|
|
190
|
+
for (const tbl of topTables) {
|
|
191
|
+
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
192
|
+
if (canSeat >= tbl.minCapacity || canSeat >= guestsNeeded) {
|
|
193
|
+
currentSet.push(tbl);
|
|
194
|
+
findMultiTableCombination(tables, guestsNeeded - canSeat, startIndex + 1, currentSet, best);
|
|
195
|
+
currentSet.pop();
|
|
196
|
+
|
|
197
|
+
if (best.tableCount === 1) return; // Found optimal
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Standard backtracking for remaining cases
|
|
183
204
|
for (let i = startIndex; i < tables.length; i++) {
|
|
184
205
|
const tbl = tables[i];
|
|
185
|
-
|
|
186
|
-
// --- Core Checks ---
|
|
187
|
-
// 1. Check if temporary table is valid for this date/time
|
|
188
|
-
if (!isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr)) {
|
|
189
|
-
continue; // Skip invalid temporary table
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 2. Check if table is free for all required slots
|
|
193
|
-
if (!isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots)) {
|
|
194
|
-
continue; // Skip occupied table
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 3. Check if table contributes meaningfully
|
|
198
206
|
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
207
|
+
|
|
208
|
+
// Skip if table can't contribute meaningfully
|
|
199
209
|
if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) {
|
|
200
|
-
continue;
|
|
210
|
+
continue;
|
|
201
211
|
}
|
|
202
|
-
|
|
203
|
-
if (canSeat <= 0) continue; // Table doesn't help
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
requiredSlots,
|
|
216
|
-
tableOccupiedSlots,
|
|
217
|
-
reservationDateStr,
|
|
218
|
-
reservationTimeStr
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
currentSet.pop(); // Backtrack: remove table to explore other combinations
|
|
213
|
+
if (canSeat <= 0) continue;
|
|
214
|
+
|
|
215
|
+
currentSet.push(tbl);
|
|
216
|
+
findMultiTableCombination(tables, guestsNeeded - canSeat, i + 1, currentSet, best);
|
|
217
|
+
currentSet.pop();
|
|
218
|
+
|
|
219
|
+
// EARLY EXIT: Found optimal single or two-table solution
|
|
220
|
+
if (best.tableCount <= 2) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
|
|
226
227
|
console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
// FIXED: More robust parsing of settings with detailed logging
|
|
229
230
|
const generalSettings = restaurantData["general-settings"] || {};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Check what format the data is in
|
|
233
|
-
console.log("[assignTablesForGivenTime] duurReservatie format:", typeof generalSettings.duurReservatie);
|
|
234
|
-
console.log("[assignTablesForGivenTime] intervalReservatie format:", typeof generalSettings.intervalReservatie);
|
|
235
|
-
|
|
236
|
-
// Check for MongoDB format
|
|
237
|
-
if (typeof generalSettings.duurReservatie === 'object' && generalSettings.duurReservatie?.$numberInt) {
|
|
238
|
-
console.log("[assignTablesForGivenTime] duurReservatie is in MongoDB format:", generalSettings.duurReservatie?.$numberInt);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (typeof generalSettings.intervalReservatie === 'object' && generalSettings.intervalReservatie?.$numberInt) {
|
|
242
|
-
console.log("[assignTablesForGivenTime] intervalReservatie is in MongoDB format:", generalSettings.intervalReservatie?.$numberInt);
|
|
243
|
-
}
|
|
244
|
-
|
|
231
|
+
|
|
245
232
|
// DEFAULT VALUES - this approach mirrors the TypeScript implementation
|
|
246
233
|
let duurReservatie = 120; // Default: 2 hours in minutes
|
|
247
234
|
let intervalReservatie = 15; // Default: 15 minute intervals
|
|
248
|
-
|
|
235
|
+
|
|
249
236
|
// Use safeParseInt for robust parsing
|
|
250
237
|
duurReservatie = safeParseInt(generalSettings.duurReservatie, 120);
|
|
251
238
|
intervalReservatie = safeParseInt(generalSettings.intervalReservatie, 15);
|
|
@@ -256,7 +243,7 @@ function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccup
|
|
|
256
243
|
console.error("Invalid interval settings.");
|
|
257
244
|
return [];
|
|
258
245
|
}
|
|
259
|
-
|
|
246
|
+
|
|
260
247
|
const requiredSlots = computeRequiredSlots(time, duurReservatie, intervalReservatie);
|
|
261
248
|
if (!requiredSlots || requiredSlots.length === 0) {
|
|
262
249
|
console.error(`Could not compute required slots for ${time}`);
|
|
@@ -267,17 +254,33 @@ function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccup
|
|
|
267
254
|
const allTables = getAllTables(restaurantData, selectedZitplaats);
|
|
268
255
|
console.log(`[assignTablesForGivenTime] Checking ${allTables.length} tables`);
|
|
269
256
|
|
|
257
|
+
// OPTIMIZATION: Pre-filter and sort tables for better performance
|
|
258
|
+
// Filter out invalid and occupied tables first
|
|
259
|
+
const validTables = allTables.filter(t =>
|
|
260
|
+
isTemporaryTableValid(t, date, time) &&
|
|
261
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (validTables.length === 0) {
|
|
265
|
+
console.log(`[assignTablesForGivenTime] No valid tables available`);
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// OPTIMIZATION: Sort tables by capacity (prefer exact matches first)
|
|
270
|
+
// This helps find optimal solutions faster
|
|
271
|
+
validTables.sort((a, b) => {
|
|
272
|
+
// Prioritize tables that can seat exactly the number of guests
|
|
273
|
+
const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
|
|
274
|
+
const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
|
|
275
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
276
|
+
|
|
277
|
+
// Then sort by capacity (smaller tables first to minimize waste)
|
|
278
|
+
return a.maxCapacity - b.maxCapacity;
|
|
279
|
+
});
|
|
280
|
+
|
|
270
281
|
// --- Try single-table assignment first ---
|
|
271
|
-
for (const t of
|
|
272
|
-
|
|
273
|
-
if (!isTemporaryTableValid(t, date, time)) {
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (t.minCapacity <= guests &&
|
|
278
|
-
guests <= t.maxCapacity &&
|
|
279
|
-
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
280
|
-
{
|
|
282
|
+
for (const t of validTables) {
|
|
283
|
+
if (t.minCapacity <= guests && guests <= t.maxCapacity) {
|
|
281
284
|
console.log(`[assignTablesForGivenTime] Assigned single table ${t.tableNumber} for ${guests} guests`);
|
|
282
285
|
return [t.tableNumber];
|
|
283
286
|
}
|
|
@@ -288,15 +291,11 @@ function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccup
|
|
|
288
291
|
// --- Try multi-table assignment ---
|
|
289
292
|
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
290
293
|
findMultiTableCombination(
|
|
291
|
-
|
|
294
|
+
validTables, // Use pre-filtered and sorted tables
|
|
292
295
|
guests,
|
|
293
296
|
0, // Start index
|
|
294
297
|
[], // Initial empty set
|
|
295
|
-
best
|
|
296
|
-
requiredSlots,
|
|
297
|
-
tableOccupiedSlots,
|
|
298
|
-
date, // Pass context
|
|
299
|
-
time // Pass context
|
|
298
|
+
best // Best solution object (no need for slots/occupancy - already filtered)
|
|
300
299
|
);
|
|
301
300
|
|
|
302
301
|
if (best.tables.length > 0) {
|
|
@@ -409,14 +408,28 @@ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, s
|
|
|
409
408
|
const allTables = getAllTables(restaurantData, selectedZitplaats); // Get all tables
|
|
410
409
|
console.log(`[isTimeAvailableSync] Checking ${allTables.length} tables for new reservation`);
|
|
411
410
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
411
|
+
// OPTIMIZATION: Pre-filter valid tables
|
|
412
|
+
const validTables = allTables.filter(t =>
|
|
413
|
+
isTemporaryTableValid(t, date, time) &&
|
|
414
|
+
isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots)
|
|
415
|
+
);
|
|
415
416
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
if (validTables.length === 0) {
|
|
418
|
+
console.log(`[isTimeAvailableSync] No valid tables available`);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// OPTIMIZATION: Sort for faster exact matches
|
|
423
|
+
validTables.sort((a, b) => {
|
|
424
|
+
const aExact = (a.minCapacity <= guests && guests <= a.maxCapacity) ? 0 : 1;
|
|
425
|
+
const bExact = (b.minCapacity <= guests && guests <= b.maxCapacity) ? 0 : 1;
|
|
426
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
427
|
+
return a.maxCapacity - b.maxCapacity;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// 1. Try single table assignment
|
|
431
|
+
for (const t of validTables) {
|
|
432
|
+
if (t.minCapacity <= guests && guests <= t.maxCapacity) {
|
|
420
433
|
console.log(`[isTimeAvailableSync] Found single table ${t.tableNumber} for ${guests} guests at ${time}`);
|
|
421
434
|
return true; // Available
|
|
422
435
|
}
|
|
@@ -427,12 +440,11 @@ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, s
|
|
|
427
440
|
// 2. Try multi-table assignment
|
|
428
441
|
const best = { minDistance: Infinity, tables: [], tableCount: Infinity };
|
|
429
442
|
findMultiTableCombination(
|
|
430
|
-
|
|
443
|
+
validTables, // Use pre-filtered tables
|
|
431
444
|
guests,
|
|
432
|
-
0,
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
date, time
|
|
445
|
+
0, // Start index
|
|
446
|
+
[], // Empty current set
|
|
447
|
+
best // Best solution
|
|
436
448
|
);
|
|
437
449
|
|
|
438
450
|
const result = best.tables.length > 0;
|
|
@@ -441,7 +453,7 @@ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, s
|
|
|
441
453
|
} else {
|
|
442
454
|
console.log(`[isTimeAvailableSync] No table combination available`);
|
|
443
455
|
}
|
|
444
|
-
|
|
456
|
+
|
|
445
457
|
return result; // Available if a combination was found
|
|
446
458
|
|
|
447
459
|
} catch (error) {
|