@happychef/algorithm 1.1.2 → 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 +152 -58
package/package.json
CHANGED
|
@@ -135,97 +135,191 @@ function calculateDistance(tableA, tableB) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* -
|
|
142
|
-
*
|
|
143
|
-
* -
|
|
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)
|
|
144
147
|
*/
|
|
148
|
+
|
|
149
|
+
// Cache for intermediate results (dynamic programming)
|
|
150
|
+
const assignmentCache = new Map();
|
|
151
|
+
|
|
145
152
|
function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best) {
|
|
146
153
|
// Base case: All guests seated
|
|
147
154
|
if (guestsNeeded <= 0) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (let
|
|
152
|
-
|
|
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;
|
|
153
168
|
}
|
|
154
|
-
}
|
|
155
|
-
// Update if better (fewer tables, or same count with lower distance)
|
|
156
|
-
if (currentSet.length < best.tableCount || (currentSet.length === best.tableCount && distanceSum < best.minDistance)) {
|
|
157
|
-
best.minDistance = distanceSum;
|
|
158
|
-
best.tables = [...currentSet];
|
|
159
|
-
best.tableCount = currentSet.length;
|
|
160
169
|
}
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
// PRUNING
|
|
165
|
-
if (currentSet.length >= best.tableCount && best.tableCount !== Infinity)
|
|
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
|
+
}
|
|
166
186
|
return;
|
|
167
187
|
}
|
|
168
188
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
205
|
+
}
|
|
178
206
|
}
|
|
179
|
-
|
|
180
|
-
|
|
207
|
+
|
|
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();
|
|
216
|
+
|
|
217
|
+
if (best.tableCount === 1) return; // Found optimal
|
|
181
218
|
}
|
|
182
219
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
);
|
|
189
226
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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();
|
|
196
233
|
|
|
197
|
-
|
|
198
|
-
}
|
|
234
|
+
if (best.tableCount === 1) return;
|
|
199
235
|
}
|
|
200
|
-
return;
|
|
201
236
|
}
|
|
202
237
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
263
|
+
|
|
264
|
+
if (totalCapacity < guestsNeeded) {
|
|
265
|
+
left = mid + 1;
|
|
266
|
+
} else {
|
|
267
|
+
right = mid - 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
207
270
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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();
|
|
279
|
+
|
|
280
|
+
if (best.tableCount === 2) return; // Found optimal two-table
|
|
281
|
+
}
|
|
211
282
|
}
|
|
283
|
+
}
|
|
212
284
|
|
|
213
|
-
|
|
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);
|
|
214
289
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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();
|
|
218
299
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return;
|
|
300
|
+
if (best.tableCount <= 2) return;
|
|
301
|
+
}
|
|
222
302
|
}
|
|
223
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
|
+
}
|
|
224
313
|
}
|
|
225
314
|
|
|
226
315
|
function assignTablesForGivenTime(restaurantData, date, time, guests, tableOccupiedSlots, selectedZitplaats = null) {
|
|
227
316
|
console.log(`[assignTablesForGivenTime] Processing ${date} ${time} for ${guests} guests`);
|
|
228
317
|
|
|
318
|
+
// Clear cache if it gets too large (prevent memory leaks)
|
|
319
|
+
if (assignmentCache.size > 1000) {
|
|
320
|
+
assignmentCache.clear();
|
|
321
|
+
}
|
|
322
|
+
|
|
229
323
|
// FIXED: More robust parsing of settings with detailed logging
|
|
230
324
|
const generalSettings = restaurantData["general-settings"] || {};
|
|
231
325
|
|