Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
import { convertFromGrams, convertToGrams } from 'expo-app/features/packs/utils';
import type { PackTemplate, WeightUnit } from '../types';
import type { WeightUnit } from '@packrat/units';
import { displayWeight, normalize, parseWeightUnit } from '@packrat/units';
import type { PackTemplate } from '../types';

export const computePackTemplateWeights = (
template: Omit<PackTemplate, 'baseWeight' | 'totalWeight'>,
preferredUnit: WeightUnit = 'g',
): PackTemplate => {
// Initialize weights
let baseWeightGrams = 0;
let totalWeightGrams = 0;

// Calculate weights based on items
for (const item of template.items) {
const itemWeightInGrams = convertToGrams(item.weight, item.weightUnit) * item.quantity;

const itemWeightInGrams =
normalize(item.weight, parseWeightUnit(item.weightUnit)) * item.quantity;
totalWeightGrams += itemWeightInGrams;

if (!item.consumable && !item.worn) {
baseWeightGrams += itemWeightInGrams;
}
}

// Convert back to preferred unit
const baseWeight = convertFromGrams(baseWeightGrams, preferredUnit);
const totalWeight = convertFromGrams(totalWeightGrams, preferredUnit);

// Return updated template with computed weights
return {
...template,
baseWeight: Number(baseWeight.toFixed(2)),
totalWeight: Number(totalWeight.toFixed(2)),
baseWeight: displayWeight(baseWeightGrams, preferredUnit),
totalWeight: displayWeight(totalWeightGrams, preferredUnit),
};
};
93 changes: 22 additions & 71 deletions apps/expo/features/packs/utils/__tests__/convertFromGrams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import { describe, expect, it } from 'vitest';
import { convertFromGrams } from '../convertFromGrams';

describe('convertFromGrams', () => {
// -------------------------------------------------------------------------
// Metric conversions
// -------------------------------------------------------------------------
describe('metric conversions', () => {
it('returns same value for grams', () => {
expect(convertFromGrams(100, 'g')).toBe(100);
Expand All @@ -20,26 +17,20 @@ describe('convertFromGrams', () => {
});
});

// -------------------------------------------------------------------------
// Imperial conversions
// -------------------------------------------------------------------------
describe('imperial conversions', () => {
describe('imperial conversions (NIST avoirdupois values)', () => {
it('converts grams to ounces correctly', () => {
expect(convertFromGrams(28.35, 'oz')).toBeCloseTo(1, 2);
expect(convertFromGrams(56.7, 'oz')).toBeCloseTo(2, 1);
expect(convertFromGrams(14.175, 'oz')).toBeCloseTo(0.5, 2);
expect(convertFromGrams(28.349523125, 'oz')).toBe(1); // exact NIST value
expect(convertFromGrams(56.69904625, 'oz')).toBeCloseTo(2, 10);
expect(convertFromGrams(14.174761562, 'oz')).toBeCloseTo(0.5, 8);
});

it('converts grams to pounds correctly', () => {
expect(convertFromGrams(453.59, 'lb')).toBeCloseTo(1, 2);
expect(convertFromGrams(907.18, 'lb')).toBeCloseTo(2, 2);
expect(convertFromGrams(226.795, 'lb')).toBeCloseTo(0.5, 2);
expect(convertFromGrams(453.59237, 'lb')).toBe(1); // exact NIST value
expect(convertFromGrams(907.18474, 'lb')).toBeCloseTo(2, 10);
expect(convertFromGrams(226.796185, 'lb')).toBeCloseTo(0.5, 8);
});
});

// -------------------------------------------------------------------------
// Edge cases
// -------------------------------------------------------------------------
describe('edge cases', () => {
it('handles zero weight', () => {
expect(convertFromGrams(0, 'kg')).toBe(0);
Expand All @@ -50,111 +41,71 @@ describe('convertFromGrams', () => {

it('handles very small weights', () => {
expect(convertFromGrams(1, 'kg')).toBe(0.001);
expect(convertFromGrams(1, 'oz')).toBeCloseTo(0.0353, 4);
expect(convertFromGrams(1, 'lb')).toBeCloseTo(0.0022, 4);
expect(convertFromGrams(1, 'oz')).toBeCloseTo(0.03527, 4);
expect(convertFromGrams(1, 'lb')).toBeCloseTo(0.002205, 4);
});

it('handles very large weights', () => {
expect(convertFromGrams(1000000, 'kg')).toBe(1000);
expect(convertFromGrams(1000000, 'oz')).toBeCloseTo(35273.37, 2);
// 1,000,000 / 28.349523125 ≈ 35273.96 (NIST exact, not the old 35273.37)
expect(convertFromGrams(1000000, 'oz')).toBeCloseTo(35273.96, 1);
expect(convertFromGrams(1000000, 'lb')).toBeCloseTo(2204.62, 0);
});

it('handles negative weights', () => {
expect(convertFromGrams(-1000, 'kg')).toBe(-1);
expect(convertFromGrams(-28.35, 'oz')).toBeCloseTo(-1, 2);
expect(convertFromGrams(-453.59, 'lb')).toBeCloseTo(-1, 2);
});

it('returns original value for unknown units', () => {
expect(convertFromGrams(100, 'invalid' as any)).toBe(100);
expect(convertFromGrams(-28.349523125, 'oz')).toBeCloseTo(-1, 10);
expect(convertFromGrams(-453.59237, 'lb')).toBeCloseTo(-1, 10);
});
});

// -------------------------------------------------------------------------
// Round-trip conversions
// -------------------------------------------------------------------------
describe('round-trip conversions', () => {
it('maintains accuracy when converting to grams and back', () => {
// Convert 100g to oz and back
describe('round-trip conversions (using same factor both ways)', () => {
it('maintains exact accuracy when converting to grams and back', () => {
const ozValue = convertFromGrams(100, 'oz');
expect(ozValue).toBeCloseTo(3.527, 3);

// Convert 1 kg to lb and back
const lbValue = convertFromGrams(1000, 'lb');
expect(lbValue).toBeCloseTo(2.205, 3);
});
});

// -------------------------------------------------------------------------
// Decimal precision
// -------------------------------------------------------------------------
describe('decimal precision', () => {
it('handles decimal weights accurately', () => {
expect(convertFromGrams(1500, 'kg')).toBe(1.5);
expect(convertFromGrams(77.96, 'oz')).toBeCloseTo(2.75, 2);
expect(convertFromGrams(1511.97, 'lb')).toBeCloseTo(3.333, 3);
});

it('preserves precision for very precise decimal inputs', () => {
expect(convertFromGrams(123.456, 'kg')).toBeCloseTo(0.123456, 6);
expect(convertFromGrams(123.456, 'oz')).toBeCloseTo(4.355, 2);
expect(convertFromGrams(123.456, 'lb')).toBeCloseTo(0.272, 3);
});
});

// -------------------------------------------------------------------------
// Real-world scenarios
// -------------------------------------------------------------------------
describe('real-world scenarios', () => {
it('converts typical camping gear weights correctly', () => {
// Tent: ~1814g to lbs
expect(convertFromGrams(1814.368, 'lb')).toBeCloseTo(4, 1);

// Sleeping bag: ~1134g to lbs
expect(convertFromGrams(1133.98, 'lb')).toBeCloseTo(2.5, 1);

// Water bottle: ~680g to oz
expect(convertFromGrams(1814.369, 'lb')).toBeCloseTo(4, 1);
expect(convertFromGrams(1133.981, 'lb')).toBeCloseTo(2.5, 1);
expect(convertFromGrams(680.388, 'oz')).toBeCloseTo(24, 0);

// Backpack: 1500g to kg
expect(convertFromGrams(1500, 'kg')).toBe(1.5);
});

it('handles ultralight gear weights', () => {
// Ultralight tarp: ~227g to oz
expect(convertFromGrams(226.796, 'oz')).toBeCloseTo(8, 0);

// Titanium spork: ~14g to oz
expect(convertFromGrams(14.1748, 'oz')).toBeCloseTo(0.5, 1);

// Down jacket: 250g
expect(convertFromGrams(250, 'g')).toBe(250);
});

it('converts between metric and imperial for common weights', () => {
// 1 lb = ~454g, back to oz (16 oz)
expect(convertFromGrams(453.59, 'oz')).toBeCloseTo(16, 0);

// 1 kg = 1000g, to lbs (~2.2 lbs)
expect(convertFromGrams(1000, 'lb')).toBeCloseTo(2.2046, 4);

// 1 oz = ~28.35g, to kg (~0.028 kg)
expect(convertFromGrams(28.35, 'kg')).toBeCloseTo(0.02835, 5);
// 1 kg in lbs
expect(convertFromGrams(1000, 'lb')).toBeCloseTo(2.20462, 4);
// 1 oz in kg
expect(convertFromGrams(28.349523125, 'kg')).toBeCloseTo(0.02835, 5);
});
});

// -------------------------------------------------------------------------
// Display formatting scenarios
// -------------------------------------------------------------------------
describe('display formatting scenarios', () => {
it('provides sensible values for UI display', () => {
// Items typically shown in oz should have reasonable precision
const heavyItem = convertFromGrams(1000, 'oz'); // ~35 oz
const heavyItem = convertFromGrams(1000, 'oz'); // ~35.27 oz
expect(heavyItem).toBeGreaterThan(35);
expect(heavyItem).toBeLessThan(36);

// Items shown in kg should be easily readable
const lightKg = convertFromGrams(250, 'kg'); // 0.25 kg
expect(lightKg).toBe(0.25);
});
Expand Down
71 changes: 15 additions & 56 deletions apps/expo/features/packs/utils/__tests__/convertToGrams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import { describe, expect, it } from 'vitest';
import { convertToGrams } from '../convertToGrams';

describe('convertToGrams', () => {
// -------------------------------------------------------------------------
// Metric conversions
// -------------------------------------------------------------------------
describe('metric conversions', () => {
it('returns same value for grams', () => {
expect(convertToGrams(100, 'g')).toBe(100);
Expand All @@ -17,41 +14,22 @@ describe('convertToGrams', () => {
expect(convertToGrams(2.5, 'kg')).toBe(2500);
expect(convertToGrams(0.1, 'kg')).toBe(100);
});

it('handles case insensitive metric units', () => {
expect(convertToGrams(1, 'KG')).toBe(1000);
expect(convertToGrams(1, 'Kg')).toBe(1000);
expect(convertToGrams(100, 'G')).toBe(100);
});
});

// -------------------------------------------------------------------------
// Imperial conversions
// -------------------------------------------------------------------------
describe('imperial conversions', () => {
describe('imperial conversions (NIST avoirdupois values)', () => {
it('converts ounces to grams correctly', () => {
expect(convertToGrams(1, 'oz')).toBeCloseTo(28.3495, 4);
expect(convertToGrams(16, 'oz')).toBeCloseTo(453.592, 2); // 1 pound
expect(convertToGrams(0.5, 'oz')).toBeCloseTo(14.1748, 4);
expect(convertToGrams(1, 'oz')).toBe(28.349523125);
expect(convertToGrams(16, 'oz')).toBeCloseTo(453.59237, 4); // 1 lb worth
expect(convertToGrams(0.5, 'oz')).toBeCloseTo(14.174761562, 6);
});

it('converts pounds to grams correctly', () => {
expect(convertToGrams(1, 'lb')).toBeCloseTo(453.592, 3);
expect(convertToGrams(2, 'lb')).toBeCloseTo(907.184, 3);
expect(convertToGrams(0.5, 'lb')).toBeCloseTo(226.796, 3);
});

it('handles case insensitive imperial units', () => {
expect(convertToGrams(1, 'OZ')).toBeCloseTo(28.3495, 4);
expect(convertToGrams(1, 'Oz')).toBeCloseTo(28.3495, 4);
expect(convertToGrams(1, 'LB')).toBeCloseTo(453.592, 3);
expect(convertToGrams(1, 'Lb')).toBeCloseTo(453.592, 3);
expect(convertToGrams(1, 'lb')).toBe(453.59237);
expect(convertToGrams(2, 'lb')).toBeCloseTo(907.18474, 4);
expect(convertToGrams(0.5, 'lb')).toBeCloseTo(226.796185, 4);
});
});

// -------------------------------------------------------------------------
// Edge cases
// -------------------------------------------------------------------------
describe('edge cases', () => {
it('handles zero weight', () => {
expect(convertToGrams(0, 'kg')).toBe(0);
Expand All @@ -61,68 +39,49 @@ describe('convertToGrams', () => {

it('handles very small weights', () => {
expect(convertToGrams(0.001, 'kg')).toBe(1);
expect(convertToGrams(0.001, 'oz')).toBeCloseTo(0.0283495, 7);
expect(convertToGrams(0.001, 'oz')).toBeCloseTo(0.028349523125, 8);
});

it('handles very large weights', () => {
expect(convertToGrams(1000, 'kg')).toBe(1000000);
expect(convertToGrams(1000, 'lb')).toBeCloseTo(453592, 0);
expect(convertToGrams(1000, 'lb')).toBeCloseTo(453592.37, 1);
});

it('handles negative weights', () => {
expect(convertToGrams(-1, 'kg')).toBe(-1000);
expect(convertToGrams(-1, 'oz')).toBeCloseTo(-28.3495, 4);
expect(convertToGrams(-1, 'oz')).toBe(-28.349523125);
});

it('returns original value for unknown units', () => {
it('returns weight unchanged for unknown units (falls back to g)', () => {
expect(convertToGrams(100, 'invalid')).toBe(100);
expect(convertToGrams(100, '')).toBe(100);
expect(convertToGrams(100, 'stone')).toBe(100);
});
});

// -------------------------------------------------------------------------
// Decimal precision
// -------------------------------------------------------------------------
describe('decimal precision', () => {
it('handles decimal weights accurately', () => {
expect(convertToGrams(1.5, 'kg')).toBe(1500);
expect(convertToGrams(2.75, 'oz')).toBeCloseTo(77.9611, 4);
expect(convertToGrams(2.75, 'oz')).toBeCloseTo(77.961188, 4);
expect(convertToGrams(3.333, 'lb')).toBeCloseTo(1511.82, 2);
});

it('preserves precision for very precise decimal inputs', () => {
expect(convertToGrams(0.123456, 'kg')).toBeCloseTo(123.456, 3);
expect(convertToGrams(0.123456, 'oz')).toBeCloseTo(3.499, 2);
});
});

// -------------------------------------------------------------------------
// Real-world scenarios
// -------------------------------------------------------------------------
describe('real-world scenarios', () => {
it('converts typical camping gear weights correctly', () => {
// Tent: 4 lbs
expect(convertToGrams(4, 'lb')).toBeCloseTo(1814.368, 2);

// Sleeping bag: 2.5 lbs
expect(convertToGrams(2.5, 'lb')).toBeCloseTo(1133.98, 2);

// Water bottle: 24 oz
expect(convertToGrams(4, 'lb')).toBeCloseTo(1814.369, 2);
expect(convertToGrams(2.5, 'lb')).toBeCloseTo(1133.981, 2);
expect(convertToGrams(24, 'oz')).toBeCloseTo(680.388, 2);

// Backpack: 1.5 kg
expect(convertToGrams(1.5, 'kg')).toBe(1500);
});

it('handles ultralight gear weights', () => {
// Ultralight tarp: 8 oz
expect(convertToGrams(8, 'oz')).toBeCloseTo(226.796, 2);

// Titanium spork: 0.5 oz
expect(convertToGrams(0.5, 'oz')).toBeCloseTo(14.1748, 4);

// Down jacket: 250g
expect(convertToGrams(0.5, 'oz')).toBeCloseTo(14.1748, 3);
expect(convertToGrams(250, 'g')).toBe(250);
});
});
Expand Down
Loading
Loading