@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happychef/algorithm",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Restaurant and reservation algorithm utilities",
5
5
  "main": "index.js",
6
6
  "author": "happy chef",
@@ -135,106 +135,191 @@ function calculateDistance(tableA, tableB) {
135
135
  }
136
136
 
137
137
  /**
138
- * OPTIMIZED: Backtracking function to find a combination of tables (multi-table assignment).
139
- * Improvements:
140
- * - Pre-computed valid tables list to avoid redundant checks
141
- * - Better pruning with capacity tracking
142
- * - Early termination when exact match found
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
- function findMultiTableCombination(tables, guestsNeeded, startIndex, currentSet, best, requiredSlots, tableOccupiedSlots, reservationDateStr, reservationTimeStr) {
145
- // Base case: All guests are seated
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
- // Calculate total distance for the current combination
148
- let distanceSum = 0;
149
- for (let i = 0; i < currentSet.length; i++) {
150
- for (let j = i + 1; j < currentSet.length; j++) {
151
- distanceSum += calculateDistance(currentSet[i], currentSet[j]);
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
- if (startIndex >= tables.length) {
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
- // OPTIMIZATION: Prune if we already have enough tables in current set
169
- if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) {
170
- return; // Can't improve on current best
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
- // OPTIMIZATION: Calculate max possible capacity from remaining tables
174
- let maxPossibleCapacity = 0;
175
- for (let i = startIndex; i < tables.length; i++) {
176
- const tbl = tables[i];
177
- // Only consider valid & free tables for potential capacity
178
- if (isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr) &&
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
- if (maxPossibleCapacity < guestsNeeded) {
186
- return; // Impossible to seat remaining guests
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
- for (let i = startIndex; i < tables.length; i++) {
190
- const tbl = tables[i];
217
+ if (best.tableCount === 1) return; // Found optimal
218
+ }
191
219
 
192
- // --- Core Checks ---
193
- // 1. Check if temporary table is valid for this date/time
194
- if (!isTemporaryTableValid(tbl, reservationDateStr, reservationTimeStr)) {
195
- continue; // Skip invalid temporary table
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
- // 2. Check if table is free for all required slots
199
- if (!isTableFreeForAllSlots(tbl.tableNumber, requiredSlots, tableOccupiedSlots)) {
200
- continue; // Skip occupied table
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
- // 3. Check if table contributes meaningfully
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
- if (canSeat <= 0) continue; // Table doesn't help
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
- // --- Recurse ---
212
- currentSet.push(tbl); // Add table to current combination
264
+ if (totalCapacity < guestsNeeded) {
265
+ left = mid + 1;
266
+ } else {
267
+ right = mid - 1;
268
+ }
269
+ }
213
270
 
214
- findMultiTableCombination(
215
- tables,
216
- guestsNeeded - canSeat, // Reduce guests needed
217
- i + 1, // Explore next tables
218
- currentSet,
219
- best,
220
- requiredSlots,
221
- tableOccupiedSlots,
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
- currentSet.pop(); // Backtrack: remove table to explore other combinations
280
+ if (best.tableCount === 2) return; // Found optimal two-table
281
+ }
282
+ }
283
+ }
227
284
 
228
- // OPTIMIZATION: Early exit if we found a single-table solution (best possible)
229
- if (best.tableCount === 1) {
230
- return;
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, // Best solution object
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, [], best,
459
- requiredSlots,
460
- tableOccupiedSlots, // Use the final occupancy map
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;