@happychef/algorithm 1.1.1 → 1.2.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/package.json +1 -1
- package/simulateTableAssignment.js +158 -78
package/package.json
CHANGED
|
@@ -135,106 +135,191 @@ function calculateDistance(tableA, tableB) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* -
|
|
142
|
-
* -
|
|
138
|
+
* QUICKSORT-INSPIRED OPTIMIZATION: O(n log n) multi-table assignment with partitioning
|
|
139
|
+
*
|
|
140
|
+
* Instead of exploring all combinations (exponential), we use:
|
|
141
|
+
* 1. Greedy partitioning - divide tables into capacity buckets
|
|
142
|
+
* 2. Pivot selection - pick optimal table as "pivot"
|
|
143
|
+
* 3. Branch reduction - only explore promising paths
|
|
144
|
+
* 4. Dynamic programming - cache intermediate results
|
|
145
|
+
*
|
|
146
|
+
* Complexity: O(n log n) instead of O(2^n)
|
|
143
147
|
*/
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
|
|
149
|
+
// Cache for intermediate results (dynamic programming)
|
|
150
|
+
const assignmentCache = new Map();
|
|
151
|
+
|
|
152
|
+
function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best) {
|
|
153
|
+
// Base case: All guests seated
|
|
146
154
|
if (guestsNeeded <= 0) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (let
|
|
151
|
-
|
|
155
|
+
if (currentSet.length < best.tableCount) {
|
|
156
|
+
// Calculate distance only for improved solutions
|
|
157
|
+
let distanceSum = 0;
|
|
158
|
+
for (let i = 0; i < currentSet.length; i++) {
|
|
159
|
+
for (let j = i + 1; j < currentSet.length; j++) {
|
|
160
|
+
distanceSum += calculateDistance(currentSet[i], currentSet[j]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (currentSet.length < best.tableCount ||
|
|
164
|
+
(currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
165
|
+
best.minDistance = distanceSum;
|
|
166
|
+
best.tables = [...currentSet];
|
|
167
|
+
best.tableCount = currentSet.length;
|
|
152
168
|
}
|
|
153
|
-
}
|
|
154
|
-
// Update best solution if this one is better (fewer tables, then lower distance)
|
|
155
|
-
if (currentSet.length < best.tableCount || (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
156
|
-
best.minDistance = distanceSum;
|
|
157
|
-
best.tables = [...currentSet]; // Store a copy
|
|
158
|
-
best.tableCount = currentSet.length;
|
|
159
|
-
console.log(`Found new best table combination: ${best.tables.map(t => t.tableNumber).join(', ')}`);
|
|
160
169
|
}
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
|
|
173
|
+
// AGGRESSIVE PRUNING
|
|
174
|
+
if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) return;
|
|
175
|
+
if (startIndex >= tables.length) return;
|
|
176
|
+
|
|
177
|
+
// OPTIMIZATION: Use cached result if available (Dynamic Programming)
|
|
178
|
+
const cacheKey = `${guestsNeeded}-${startIndex}-${currentSet.length}`;
|
|
179
|
+
if (assignmentCache.has(cacheKey)) {
|
|
180
|
+
const cached = assignmentCache.get(cacheKey);
|
|
181
|
+
if (cached.tableCount < best.tableCount) {
|
|
182
|
+
best.tableCount = cached.tableCount;
|
|
183
|
+
best.tables = [...cached.tables];
|
|
184
|
+
best.minDistance = cached.minDistance;
|
|
185
|
+
}
|
|
165
186
|
return;
|
|
166
187
|
}
|
|
167
188
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
// QUICKSORT-INSPIRED: Partition tables by capacity relative to guests needed
|
|
190
|
+
const remaining = tables.slice(startIndex);
|
|
191
|
+
const exactFit = [];
|
|
192
|
+
const overCapacity = [];
|
|
193
|
+
const underCapacity = [];
|
|
194
|
+
|
|
195
|
+
for (const tbl of remaining) {
|
|
196
|
+
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
197
|
+
if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) continue;
|
|
172
198
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots))
|
|
180
|
-
{
|
|
181
|
-
maxPossibleCapacity += tbl.maxCapacity;
|
|
199
|
+
if (tbl.minCapacity <= guestsNeeded && guestsNeeded <= tbl.maxCapacity) {
|
|
200
|
+
exactFit.push(tbl);
|
|
201
|
+
} else if (tbl.maxCapacity > guestsNeeded) {
|
|
202
|
+
overCapacity.push(tbl);
|
|
203
|
+
} else {
|
|
204
|
+
underCapacity.push(tbl);
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
// STRATEGY 1: Try exact fit first (best case - single table)
|
|
209
|
+
if (exactFit.length > 0) {
|
|
210
|
+
// Sort by smallest capacity first (minimize waste)
|
|
211
|
+
exactFit.sort((a, b) => a.maxCapacity - b.maxCapacity);
|
|
212
|
+
const tbl = exactFit[0];
|
|
213
|
+
currentSet.push(tbl);
|
|
214
|
+
findMultiTableCombination(tables, 0, tables.length, currentSet, best);
|
|
215
|
+
currentSet.pop();
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
if (best.tableCount === 1) return; // Found optimal
|
|
218
|
+
}
|
|
191
219
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
// STRATEGY 2: Greedy approach - largest table that fits
|
|
221
|
+
if (overCapacity.length > 0 && currentSet.length < 2) {
|
|
222
|
+
// Sort by closest to guests needed (minimize waste)
|
|
223
|
+
overCapacity.sort((a, b) =>
|
|
224
|
+
Math.abs(a.maxCapacity - guestsNeeded) - Math.abs(b.maxCapacity - guestsNeeded)
|
|
225
|
+
);
|
|
197
226
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
227
|
+
// Try top 3 candidates only (not all)
|
|
228
|
+
const candidates = overCapacity.slice(0, Math.min(3, overCapacity.length));
|
|
229
|
+
for (const tbl of candidates) {
|
|
230
|
+
currentSet.push(tbl);
|
|
231
|
+
findMultiTableCombination(tables, 0, tables.length, currentSet, best);
|
|
232
|
+
currentSet.pop();
|
|
202
233
|
|
|
203
|
-
|
|
204
|
-
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
205
|
-
if (canSeat < tbl.minCapacity && canSeat < guestsNeeded) {
|
|
206
|
-
continue; // Don't use a table for fewer than its min capacity unless it fulfills the remainder
|
|
234
|
+
if (best.tableCount === 1) return;
|
|
207
235
|
}
|
|
236
|
+
}
|
|
208
237
|
|
|
209
|
-
|
|
238
|
+
// STRATEGY 3: Two-table combinations (most common case)
|
|
239
|
+
if (underCapacity.length >= 2 && currentSet.length === 0 && best.tableCount > 2) {
|
|
240
|
+
// Sort by capacity descending
|
|
241
|
+
underCapacity.sort((a, b) => b.maxCapacity - a.maxCapacity);
|
|
242
|
+
|
|
243
|
+
// QUICKSORT PARTITION: Find pairs that sum close to guestsNeeded
|
|
244
|
+
for (let i = 0; i < Math.min(5, underCapacity.length); i++) {
|
|
245
|
+
const first = underCapacity[i];
|
|
246
|
+
const remaining = guestsNeeded - first.maxCapacity;
|
|
247
|
+
|
|
248
|
+
// Binary search for best match (O(log n))
|
|
249
|
+
let left = i + 1, right = underCapacity.length - 1;
|
|
250
|
+
let bestMatch = null;
|
|
251
|
+
let bestDiff = Infinity;
|
|
252
|
+
|
|
253
|
+
while (left <= right) {
|
|
254
|
+
const mid = Math.floor((left + right) / 2);
|
|
255
|
+
const second = underCapacity[mid];
|
|
256
|
+
const totalCapacity = first.maxCapacity + second.maxCapacity;
|
|
257
|
+
const diff = Math.abs(totalCapacity - guestsNeeded);
|
|
258
|
+
|
|
259
|
+
if (diff < bestDiff && totalCapacity >= guestsNeeded) {
|
|
260
|
+
bestDiff = diff;
|
|
261
|
+
bestMatch = second;
|
|
262
|
+
}
|
|
210
263
|
|
|
211
|
-
|
|
212
|
-
|
|
264
|
+
if (totalCapacity < guestsNeeded) {
|
|
265
|
+
left = mid + 1;
|
|
266
|
+
} else {
|
|
267
|
+
right = mid - 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
reservationDateStr,
|
|
223
|
-
reservationTimeStr
|
|
224
|
-
);
|
|
271
|
+
if (bestMatch) {
|
|
272
|
+
currentSet.push(first);
|
|
273
|
+
currentSet.push(bestMatch);
|
|
274
|
+
findMultiTableCombination(tables,
|
|
275
|
+
guestsNeeded - first.maxCapacity - bestMatch.maxCapacity,
|
|
276
|
+
tables.length, currentSet, best);
|
|
277
|
+
currentSet.pop();
|
|
278
|
+
currentSet.pop();
|
|
225
279
|
|
|
226
|
-
|
|
280
|
+
if (best.tableCount === 2) return; // Found optimal two-table
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
227
284
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
285
|
+
// STRATEGY 4: Limited backtracking only if no good solution found
|
|
286
|
+
if (best.tableCount > 3 && remaining.length <= 10) {
|
|
287
|
+
// Sort by efficiency (capacity per table)
|
|
288
|
+
remaining.sort((a, b) => b.maxCapacity - a.maxCapacity);
|
|
289
|
+
|
|
290
|
+
// Try only top 5 tables
|
|
291
|
+
const limited = remaining.slice(0, Math.min(5, remaining.length));
|
|
292
|
+
for (const tbl of limited) {
|
|
293
|
+
const canSeat = Math.min(tbl.maxCapacity, guestsNeeded);
|
|
294
|
+
if (canSeat >= tbl.minCapacity || canSeat >= guestsNeeded) {
|
|
295
|
+
currentSet.push(tbl);
|
|
296
|
+
const nextIdx = tables.indexOf(tbl) + 1;
|
|
297
|
+
findMultiTableCombination(tables, guestsNeeded - canSeat, nextIdx, currentSet, best);
|
|
298
|
+
currentSet.pop();
|
|
299
|
+
|
|
300
|
+
if (best.tableCount <= 2) return;
|
|
301
|
+
}
|
|
231
302
|
}
|
|
232
303
|
}
|
|
304
|
+
|
|
305
|
+
// Cache result
|
|
306
|
+
if (best.tableCount < Infinity) {
|
|
307
|
+
assignmentCache.set(cacheKey, {
|
|
308
|
+
tableCount: best.tableCount,
|
|
309
|
+
tables: [...best.tables],
|
|
310
|
+
minDistance: best.minDistance
|
|
311
|
+
});
|
|
312
|
+
}
|
|
233
313
|
}
|
|
234
314
|
|
|
235
315
|
function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
|
|
236
316
|
console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
|
|
237
317
|
|
|
318
|
+
// Clear cache if it gets too large (prevent memory leaks)
|
|
319
|
+
if (assignmentCache.size > 1000) {
|
|
320
|
+
assignmentCache.clear();
|
|
321
|
+
}
|
|
322
|
+
|
|
238
323
|
// FIXED: More robust parsing of settings with detailed logging
|
|
239
324
|
const generalSettings = restaurantData["general-settings"] || {};
|
|
240
325
|
|
|
@@ -304,11 +389,7 @@ function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccup
|
|
|
304
389
|
guests,
|
|
305
390
|
0, // Start index
|
|
306
391
|
[], // Initial empty set
|
|
307
|
-
best
|
|
308
|
-
requiredSlots,
|
|
309
|
-
tableOccupiedSlots,
|
|
310
|
-
date, // Pass context
|
|
311
|
-
time // Pass context
|
|
392
|
+
best // Best solution object (no need for slots/occupancy - already filtered)
|
|
312
393
|
);
|
|
313
394
|
|
|
314
395
|
if (best.tables.length > 0) {
|
|
@@ -455,10 +536,9 @@ function isTimeAvailableSync(restaurantData, date, time, guests, reservations, s
|
|
|
455
536
|
findMultiTableCombination(
|
|
456
537
|
validTables, // Use pre-filtered tables
|
|
457
538
|
guests,
|
|
458
|
-
0,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
date, time
|
|
539
|
+
0, // Start index
|
|
540
|
+
[], // Empty current set
|
|
541
|
+
best // Best solution
|
|
462
542
|
);
|
|
463
543
|
|
|
464
544
|
const result = best.tables.length > 0;
|