@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happychef/algorithm",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Restaurant and reservation algorithm utilities",
5
5
  "main": "index.js",
6
6
  "author": "happy chef",
@@ -135,10 +135,15 @@ function calculateDistance(tableA, tableB) {
135
135
  }
136
136
 
137
137
  /**
138
- * Backtracking function to find a combination of tables (multi-table assignment).
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, requiredSlots, tableOccupiedSlots, reservationDateStr, reservationTimeStr) {
141
- // Base case: All guests are seated
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 best solution if this one is better (fewer tables, then lower distance)
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]; // Store a copy
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
- if (startIndex >= tables.length) {
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
- if (currentSet.length >= best.tableCount && best.tableCount !== Infinity) {
165
- return; // Pruning based on table count
169
+ // PRUNING: Not enough tables remaining
170
+ if (startIndex >= tables.length) {
171
+ return;
166
172
  }
167
173
 
168
- let maxPossibleCapacity = 0;
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
- const tbl = tables[i];
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
- if (maxPossibleCapacity < guestsNeeded) {
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; // Don't use a table for fewer than its min capacity unless it fulfills the remainder
210
+ continue;
201
211
  }
202
-
203
- if (canSeat <= 0) continue; // Table doesn't help
204
212
 
205
- // --- Recurse ---
206
- currentSet.push(tbl); // Add table to current combination
207
- console.log(`Trying table ${tbl.tableNumber} (${tbl.minCapacity}-${tbl.maxCapacity}) for ${canSeat}/${guestsNeeded} guests`);
208
-
209
- findMultiTableCombination(
210
- tables,
211
- guestsNeeded - canSeat, // Reduce guests needed
212
- i + 1, // Explore next tables
213
- currentSet,
214
- best,
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
- console.log("[assignTablesForGivenTime] General settings:", generalSettings);
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 allTables) {
272
- // Use imported validation function
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
- allTables,
294
+ validTables, // Use pre-filtered and sorted tables
292
295
  guests,
293
296
  0, // Start index
294
297
  [], // Initial empty set
295
- best, // Best solution object
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
- // 1. Try single table assignment
413
- for (const t of allTables) {
414
- if (!isTemporaryTableValid(t, date, time)) continue; // Check temporary validity
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
- if (t.minCapacity <= guests &&
417
- guests <= t.maxCapacity &&
418
- isTableFreeForAllSlots(t.tableNumber, requiredSlots, tableOccupiedSlots))
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
- allTables,
443
+ validTables, // Use pre-filtered tables
431
444
  guests,
432
- 0, [], best,
433
- requiredSlots,
434
- tableOccupiedSlots, // Use the final occupancy map
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) {