All files / src/utils unitConverter.ts

100% Statements 32/32
100% Branches 26/26
100% Functions 3/3
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128      3x                           3x 3x                         3x 120x 120x 120x   120x 120x 22x 98x 9x     120x 89x         31x             120x   120x                       3x       121x 8x       113x   113x 113x     113x       113x                             121x 2x     111x                 3x     9x 2x       7x    
// src/utils/unitConverter.ts
import type { UnitPrice } from '../types';
 
const CONVERSIONS = {
  metric: {
    g: { to: 'oz', factor: 0.035274 },
    kg: { to: 'lb', factor: 2.20462 },
    ml: { to: 'fl oz', factor: 0.033814 },
    l: { to: 'fl oz', factor: 33.814 },
  },
  imperial: {
    oz: { to: 'g', factor: 28.3495 },
    lb: { to: 'kg', factor: 0.453592 },
    'fl oz': { to: 'ml', factor: 29.5735 },
  },
};
 
const METRIC_UNITS = Object.keys(CONVERSIONS.metric);
const IMPERIAL_UNITS = Object.keys(CONVERSIONS.imperial);
 
interface FormattedPrice {
  price: string;
  unit: string | null;
}
 
/**
 * Internal helper to convert a unit price to a target system if necessary.
 * @param unitPrice The unit price to potentially convert.
 * @param targetSystem The desired unit system.
 * @returns A new UnitPrice object if converted, otherwise the original object.
 */
const convertUnitPrice = (unitPrice: UnitPrice, targetSystem: 'metric' | 'imperial'): UnitPrice => {
  const { value, unit } = unitPrice;
  const isMetric = METRIC_UNITS.includes(unit);
  const isImperial = IMPERIAL_UNITS.includes(unit);
 
  let needsConversion = false;
  if (targetSystem === 'imperial' && isMetric) {
    needsConversion = true;
  } else if (targetSystem === 'metric' && isImperial) {
    needsConversion = true;
  }
 
  if (!needsConversion) {
    return unitPrice; // Return original object if no conversion is needed
  }
 
  // The source system is determined by which list the unit belongs to.
  // We access the specific map based on the detected system to satisfy TypeScript.
  const conversion = isMetric
    ? CONVERSIONS.metric[unit as keyof typeof CONVERSIONS.metric]
    : CONVERSIONS.imperial[unit as keyof typeof CONVERSIONS.imperial];
 
  // When converting price per unit, the factor logic is inverted.
  // e.g., to get price per lb from price per kg, you divide by the kg-to-lb factor.
  // Price/kg * (1 kg / 2.20462 lb) = (Price / 2.20462) per lb.
  const convertedValue = value / conversion.factor;
 
  return {
    value: convertedValue,
    unit: conversion.to,
  };
};
 
/**
 * Converts a unit price to the target system and formats it for display.
 * @param unitPrice The structured unit price object from the database.
 * @param system The target system ('metric' or 'imperial').
 * @returns An object with formatted price and unit strings.
 */
export const formatUnitPrice = (
  unitPrice: UnitPrice | null | undefined,
  system: 'metric' | 'imperial',
): FormattedPrice => {
  if (!unitPrice || typeof unitPrice.value !== 'number' || !unitPrice.unit) {
    return { price: '—', unit: null };
  }
 
  // First, convert the unit price to the correct system.
  const convertedUnitPrice = convertUnitPrice(unitPrice, system);
 
  const displayValue = convertedUnitPrice.value;
  const displayUnit = convertedUnitPrice.unit;
 
  // Convert the final calculated value (which is in cents) to dollars for formatting.
  const valueInDollars = displayValue / 100;
 
  // Smart formatting for price
  const formattedPrice =
    valueInDollars < 0.1
      ? valueInDollars.toLocaleString('en-US', {
          style: 'currency',
          currency: 'USD',
          minimumFractionDigits: 3,
          maximumFractionDigits: 3,
        })
      : valueInDollars.toLocaleString('en-US', {
          style: 'currency',
          currency: 'USD',
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        });
 
  // Always show a unit if one exists for clarity
  if (displayUnit === 'each') {
    return { price: formattedPrice, unit: '/each' };
  }
 
  return { price: formattedPrice, unit: `/${displayUnit}` };
};
 
/**
 * Converts an imperial unit price to its metric equivalent for database storage.
 * This is used to standardize units before saving them.
 * @param unitPrice The structured unit price object, potentially in imperial units.
 * @returns A unit price object with metric units, or the original if already metric or not applicable.
 */
export const convertToMetric = (
  unitPrice: UnitPrice | null | undefined,
): UnitPrice | null | undefined => {
  if (!unitPrice || typeof unitPrice.value !== 'number' || !unitPrice.unit) {
    return unitPrice;
  }
 
  // The logic is now simply a call to the centralized converter.
  return convertUnitPrice(unitPrice, 'metric');
};