Edited layout of mmddyyyy and time dropdown. Changed algorithm for determining pricing so that it choosest the cheapest option for users

This commit is contained in:
jackiettran
2026-01-01 14:46:40 -05:00
parent 3d0e553620
commit fd2312fe47
3 changed files with 650 additions and 210 deletions

View File

@@ -1,61 +1,143 @@
/**
* Rental Duration Calculator with Hybrid Pricing
*
* Finds the optimal (cheapest) combination of pricing tiers for any rental duration.
* Supports combining hours + days + weeks + months for best pricing.
*
* Example: 26 hours with $10/hr and $50/day
* - Old logic: 2 days × $50 = $100
* - New logic: 1 day × $50 + 2 hours × $10 = $70
*/
class RentalDurationCalculator {
// Time constants in milliseconds
static HOUR_MS = 60 * 60 * 1000;
static DAY_MS = 24 * 60 * 60 * 1000;
static WEEK_MS = 7 * 24 * 60 * 60 * 1000;
static MONTH_MS = 30 * 24 * 60 * 60 * 1000;
/**
* Build available pricing tiers from item, sorted largest to smallest
* @param {Object} item - Item object with pricing information
* @returns {Array} Array of {name, price, durationMs} sorted largest to smallest
*/
static buildPricingTiers(item) {
const tiers = [];
if (item.pricePerMonth && Number(item.pricePerMonth) > 0) {
tiers.push({
name: "month",
price: Number(item.pricePerMonth),
durationMs: this.MONTH_MS,
});
}
if (item.pricePerWeek && Number(item.pricePerWeek) > 0) {
tiers.push({
name: "week",
price: Number(item.pricePerWeek),
durationMs: this.WEEK_MS,
});
}
if (item.pricePerDay && Number(item.pricePerDay) > 0) {
tiers.push({
name: "day",
price: Number(item.pricePerDay),
durationMs: this.DAY_MS,
});
}
if (item.pricePerHour && Number(item.pricePerHour) > 0) {
tiers.push({
name: "hour",
price: Number(item.pricePerHour),
durationMs: this.HOUR_MS,
});
}
return tiers;
}
/**
* Recursively calculate optimal price for remaining duration
* Uses greedy approach: try largest tier first, then handle remainder with smaller tiers
* Compares hybrid price vs round-up price and returns the cheaper option
*
* @param {number} remainingMs - Remaining duration in milliseconds
* @param {Array} tiers - Available pricing tiers (largest to smallest)
* @param {number} tierIndex - Current tier being considered
* @returns {number} Optimal price for this duration
*/
static calculateOptimalForDuration(remainingMs, tiers, tierIndex = 0) {
// Base case: no remaining time
if (remainingMs <= 0) {
return 0;
}
// Base case: no more tiers available - round up with smallest available tier
if (tierIndex >= tiers.length) {
const smallestTier = tiers[tiers.length - 1];
const unitsNeeded = Math.ceil(remainingMs / smallestTier.durationMs);
return unitsNeeded * smallestTier.price;
}
const currentTier = tiers[tierIndex];
const completeUnits = Math.floor(remainingMs / currentTier.durationMs);
const remainder = remainingMs - completeUnits * currentTier.durationMs;
if (completeUnits > 0) {
// Option 1: Use complete units of current tier + handle remainder with smaller tiers
const currentCost = completeUnits * currentTier.price;
const remainderCost = this.calculateOptimalForDuration(
remainder,
tiers,
tierIndex + 1
);
const hybridCost = currentCost + remainderCost;
// Option 2: Round up to one more unit of current tier (may be cheaper if remainder cost is high)
const roundUpCost = (completeUnits + 1) * currentTier.price;
// Return the cheaper option when there's a remainder to handle
return remainder > 0 ? Math.min(hybridCost, roundUpCost) : currentCost;
} else {
// Duration too small for this tier, try next smaller tier
return this.calculateOptimalForDuration(remainingMs, tiers, tierIndex + 1);
}
}
/**
* Calculate rental cost based on duration and item pricing tiers
* Uses hybrid pricing to find the optimal (cheapest) combination of tiers
*
* @param {Date|string} startDateTime - Rental start date/time
* @param {Date|string} endDateTime - Rental end date/time
* @param {Object} item - Item object with pricing information
* @returns {number} Total rental cost
*/
static calculateRentalCost(startDateTime, endDateTime, item) {
const rentalStartDateTime = new Date(startDateTime);
const rentalEndDateTime = new Date(endDateTime);
const start = new Date(startDateTime);
const end = new Date(endDateTime);
// Calculate rental duration
const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime();
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
const diffWeeks = Math.ceil(diffDays / 7);
// Calculate total duration in milliseconds
const durationMs = end.getTime() - start.getTime();
// Calculate difference in calendar months
let diffMonths = (rentalEndDateTime.getFullYear() - rentalStartDateTime.getFullYear()) * 12;
diffMonths += rentalEndDateTime.getMonth() - rentalStartDateTime.getMonth();
// Add 1 if we're past the start day/time in the end month
if (rentalEndDateTime.getDate() > rentalStartDateTime.getDate()) {
diffMonths += 1;
} else if (rentalEndDateTime.getDate() === rentalStartDateTime.getDate() &&
rentalEndDateTime.getTime() > rentalStartDateTime.getTime()) {
diffMonths += 1;
if (durationMs <= 0) {
return 0;
}
diffMonths = Math.max(1, diffMonths);
// Build available pricing tiers
const tiers = this.buildPricingTiers(item);
// Calculate base amount based on duration (tiered pricing)
let totalAmount;
if (item.pricePerHour && diffHours < 24) {
// Use hourly rate for rentals under 24 hours
totalAmount = diffHours * Number(item.pricePerHour);
} else if (diffDays <= 7 && item.pricePerDay) {
// Use daily rate for rentals <= 7 days
totalAmount = diffDays * Number(item.pricePerDay);
} else if (diffMonths <= 1 && item.pricePerWeek) {
// Use weekly rate for rentals <= 1 calendar month
totalAmount = diffWeeks * Number(item.pricePerWeek);
} else if (diffMonths > 1 && item.pricePerMonth) {
// Use monthly rate for rentals > 1 calendar month
totalAmount = diffMonths * Number(item.pricePerMonth);
} else if (item.pricePerWeek) {
// Fallback to weekly rate if monthly not available
totalAmount = diffWeeks * Number(item.pricePerWeek);
} else if (item.pricePerDay) {
// Fallback to daily rate
totalAmount = diffDays * Number(item.pricePerDay);
} else {
totalAmount = 0;
// No pricing tiers = free item
if (tiers.length === 0) {
return 0;
}
return totalAmount;
// Calculate optimal price using hybrid tier combination
const optimalPrice = this.calculateOptimalForDuration(durationMs, tiers);
return parseFloat(optimalPrice.toFixed(2));
}
}