npm package: @olusiji/measurements-converter
Description: handles the conversion of measurement units
Author: Olusiji
Latest version: 1.0.1

# @olusiji/measurements-converter

`A TypeScript library for converting between measurement units` across multiple categories: **length**, **weight**, **volume**, **area**, and **unit/piece** counts.

## Installation

```bash
npm install @olusiji/measurements-converter
```

## Quick Start

```typescript
import {
  Measurement,
  MeasurementUnit,
  MeasurementUnitCategory,
} from '@olusiji/measurements-converter';

// Create a unit and a measurement
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const distance = new Measurement(meter, 5);

// Convert to feet
const foot = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const inFeet = distance.convertToMeasurementUnit(foot);
console.log(inFeet.value); // 16.404199475...
```

## API Reference

### `MeasurementUnit`

Represents a unit of measurement. Validates that the name and symbol are consistent with the category.

#### Constructor

```typescript
new MeasurementUnit(category: MeasurementUnitCategory, name: MeasurementUnitName, symbol: string)
```

#### Creating units manually

```typescript
import { MeasurementUnit, MeasurementUnitCategory } from '@olusiji/measurements-converter';

const meter      = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const centimeter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'CENTIMETER', 'cm');
const foot       = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const inch       = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'INCH', 'in');
const yard       = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'YARD', 'yd');

const kilogram  = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');
const gram      = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'GRAM', 'g');
const pound     = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'POUND', 'lb');
const milligram = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'MILLIGRAM', 'mg');

const liter      = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'LITER', 'l');
const milliliter = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'MILLILITER', 'ml');
const gallon     = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'GALLON', 'gal');
const cubicMeter = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'CUBIC_METER', 'm3');

const squareMeter = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_METER', 'm2');
const squareFoot  = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_FOOT', 'ft2');
const squareInch  = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_INCH', 'in2');
const squareYard  = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_YARD', 'yd2');

const unit  = new MeasurementUnit(MeasurementUnitCategory.UNIT, 'UNIT', 'unit');
const piece = new MeasurementUnit(MeasurementUnitCategory.UNIT, 'PIECE', 'pcs');
```

#### Creating units from a name string

```typescript
const meter = MeasurementUnit.fromUnitName('meter');     // case-insensitive
const kg    = MeasurementUnit.fromUnitName('KILOGRAM');
const gal   = MeasurementUnit.fromUnitName('Gallon');
```

#### Listing all units in a category

```typescript
const lengthUnits = MeasurementUnit.getUnitsInCategory(MeasurementUnitCategory.LENGTH);
console.log(lengthUnits.length); // 5 (METER, CENTIMETER, FOOT, INCH, YARD)

const weightUnits = MeasurementUnit.getUnitsInCategory(MeasurementUnitCategory.WEIGHT);
console.log(weightUnits.map(u => u.name)); // ['KILOGRAM', 'GRAM', 'POUND', 'MILLIGRAM']
```

#### Comparing units

```typescript
const meter1 = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const meter2 = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const foot   = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const kg     = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');

meter1.isEqualTo(meter2);  // true
meter1.isEqualTo(foot);    // false

meter1.isSameCategory(foot); // true  (both LENGTH)
meter1.isSameCategory(kg);   // false (LENGTH vs WEIGHT)
```

#### Getting the base unit and conversion factor

```typescript
const foot = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');

const base = foot.getBaseUnit();
console.log(base.name);   // 'INCH' (default base unit for LENGTH)

console.log(foot.conversionFactor); // 12 (1 foot = 12 inches)
```

---

### `Measurement`

Represents a numeric value paired with a `MeasurementUnit`.

#### Constructor

```typescript
new Measurement(unit: MeasurementUnit, value: number)
```

#### Creating measurements

```typescript
import { Measurement, MeasurementUnit, MeasurementUnitCategory } from '@olusiji/measurements-converter';

const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const distance = new Measurement(meter, 10);

console.log(distance.value);     // 10
console.log(distance.unit.name); // 'METER'
```

#### Converting to the base unit

Every category has a default base unit. `convertToBaseMeasurement()` converts any measurement to that base:

| Category | Base Unit |
|----------|-----------|
| LENGTH   | INCH      |
| WEIGHT   | KILOGRAM  |
| VOLUME   | LITER     |
| AREA     | SQUARE_INCH |
| UNIT     | UNIT      |

```typescript
const foot = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const measurement = new Measurement(foot, 3);

const base = measurement.convertToBaseMeasurement();
console.log(base.unit.name); // 'INCH'
console.log(base.value);     // 36  (3 feet × 12 inches/foot)
```

#### Converting between units

```typescript
// Meters → Feet
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const foot  = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');

const inMeters = new Measurement(meter, 2);
const inFeet   = inMeters.convertToMeasurementUnit(foot);
console.log(inFeet.value); // 6.56168...

// Kilograms → Pounds
const kg = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');
const lb = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'POUND', 'lb');

const weight   = new Measurement(kg, 5);
const inPounds = weight.convertToMeasurementUnit(lb);
console.log(inPounds.value); // 11.0231...

// Liters → Gallons
const liter  = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'LITER', 'l');
const gallon = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'GALLON', 'gal');

const volume     = new Measurement(liter, 10);
const inGallons  = volume.convertToMeasurementUnit(gallon);
console.log(inGallons.value); // 2.6417...

// Square meters → Square feet
const sqm = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_METER', 'm2');
const sqf = new MeasurementUnit(MeasurementUnitCategory.AREA, 'SQUARE_FOOT', 'ft2');

const area   = new Measurement(sqm, 4);
const inSqFt = area.convertToMeasurementUnit(sqf);
console.log(inSqFt.value); // 43.0555...
```

#### Comparing measurements

```typescript
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const foot  = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const kg    = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');

const m1 = new Measurement(meter, 5);
const m2 = new Measurement(foot, 10);
const m3 = new Measurement(kg, 2);

m1.isSameCategory(m2); // true  (both LENGTH)
m1.isSameCategory(m3); // false (LENGTH vs WEIGHT)

m1.isSameUnit(m2); // false (METER vs FOOT)
```

#### Adding measurements

You can add two measurements of the same category. The result is expressed in the base unit:

```typescript
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const foot  = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');

const a = new Measurement(meter, 1);  // 1 meter
const b = new Measurement(foot, 3);   // 3 feet

const total = a.addMeasurement(b);
console.log(total.unit.name); // 'INCH' (base unit for LENGTH)
console.log(total.value);     // 75.3701  (39.3701 + 36)
```

Attempting to add measurements of different categories throws an error:

```typescript
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const kg    = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');

const length = new Measurement(meter, 1);
const weight = new Measurement(kg, 1);

length.addMeasurement(weight); // throws InvalidMeasurement: 'Cannot add measurements of different categories'
```

---

### `MeasurementWithQuantity`

Pairs a `Measurement` with a quantity multiplier — useful for inventory or recipe scenarios.

#### Constructor

```typescript
new MeasurementWithQuantity(measurement: Measurement, quantity: number)
```

#### Total measurement

```typescript
import {
  Measurement,
  MeasurementUnit,
  MeasurementUnitCategory,
  MeasurementWithQuantity,
} from '@olusiji/measurements-converter';

const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const plank = new Measurement(meter, 2.5); // each plank is 2.5m

const order = new MeasurementWithQuantity(plank, 4); // 4 planks

const total = order.totalMeasurement();
console.log(total.value);     // 10
console.log(total.unit.name); // 'METER'
```

#### Total measurement converted to a different unit

```typescript
const foot = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');

const totalInFeet = order.totalMeasurementToUnit(foot);
console.log(totalInFeet.value);     // 32.8084...
console.log(totalInFeet.unit.name); // 'FOOT'
```

If the target unit is the same as the measurement's unit, no conversion is performed:

```typescript
const totalInMeters = order.totalMeasurementToUnit(meter);
console.log(totalInMeters.value); // 10 (no conversion needed)
```

---

### `MeasurementService`

Static utility methods for working with collections of measurements.

#### Summing measurements

Sum an array of `MeasurementWithQuantity` items into a single `Measurement` in a target unit. All items must be in the same category as the target unit.

```typescript
import {
  Measurement,
  MeasurementUnit,
  MeasurementUnitCategory,
  MeasurementWithQuantity,
  MeasurementService,
} from '@olusiji/measurements-converter';

const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const foot  = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'FOOT', 'ft');
const inch  = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'INCH', 'in');

const measurements = [
  new MeasurementWithQuantity(new Measurement(meter, 1), 1),  // 1 meter × 1
  new MeasurementWithQuantity(new Measurement(foot, 1), 1),   // 1 foot × 1
];

// Sum and express the result in inches
const total = MeasurementService.sumMeasurements(measurements, inch);
console.log(total.unit.name); // 'INCH'
console.log(total.value);     // 51.3701 (39.3701 + 12)

// Sum with quantities
const boards = [
  new MeasurementWithQuantity(new Measurement(meter, 2), 3),  // 2m × 3 boards
  new MeasurementWithQuantity(new Measurement(meter, 1), 2),  // 1m × 2 boards
];
const totalMeters = MeasurementService.sumMeasurements(boards, meter);
console.log(totalMeters.value); // 8
```

Throws if measurements span different categories:

```typescript
const kg = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');

const mixed = [
  new MeasurementWithQuantity(new Measurement(meter, 1), 1),
  new MeasurementWithQuantity(new Measurement(kg, 1), 1),
];

MeasurementService.sumMeasurements(mixed, meter);
// throws MeasurementConversionError: 'All measurements must be of the same category'
```

#### Total cost of measurements in the same category

Calculate the total cost when all measurements share a single cost per unit. You specify the target unit for summing.

```typescript
const measurements = [
  new MeasurementWithQuantity(new Measurement(meter, 2), 1),
  new MeasurementWithQuantity(new Measurement(meter, 3), 1),
];

const cost = MeasurementService.getTotalCostOfMeasurementsInSameCategory(measurements, meter, 10);
console.log(cost); // 50  (5 meters × $10/meter)
```

Works across different units in the same category:

```typescript
const kg   = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');
const gram = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'GRAM', 'g');

const ingredients = [
  new MeasurementWithQuantity(new Measurement(kg, 1), 1),    // 1 kg
  new MeasurementWithQuantity(new Measurement(gram, 500), 1), // 500 g
];

const cost = MeasurementService.getTotalCostOfMeasurementsInSameCategory(ingredients, kg, 20);
console.log(cost); // 30  (1.5 kg × $20/kg)
```

#### Total cost of measurements with individual pricing

Calculate the total cost when each item has its own cost per unit. Supports items across different categories.

```typescript
const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
const kg    = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');
const liter = new MeasurementUnit(MeasurementUnitCategory.VOLUME, 'LITER', 'l');

const totalCost = MeasurementService.getTotalCostOfMeasurements([
  {
    measurement: new MeasurementWithQuantity(new Measurement(meter, 3), 2),
    costPerUnit: 8,    // $8 per meter
  },
  {
    measurement: new MeasurementWithQuantity(new Measurement(kg, 1), 4),
    costPerUnit: 12,   // $12 per kg
  },
  {
    measurement: new MeasurementWithQuantity(new Measurement(liter, 5), 1),
    costPerUnit: 3,    // $3 per liter
  },
]);

console.log(totalCost); // 111  (6m×$8 + 4kg×$12 + 5l×$3 = $48 + $48 + $15)
```

---

### Supported Units

#### Length

| Name | Symbol | Base Unit |
|------|--------|-----------|
| METER | m | No |
| CENTIMETER | cm | No |
| FOOT | ft | No |
| INCH | in | ✅ Yes |
| YARD | yd | No |

#### Weight

| Name | Symbol | Base Unit |
|------|--------|-----------|
| KILOGRAM | kg | ✅ Yes |
| GRAM | g | No |
| POUND | lb | No |
| MILLIGRAM | mg | No |

#### Volume

| Name | Symbol | Base Unit |
|------|--------|-----------|
| LITER | l | ✅ Yes |
| MILLILITER | ml | No |
| GALLON | gal | No |
| CUBIC_METER | m3 | No |

#### Area

| Name | Symbol | Base Unit |
|------|--------|-----------|
| SQUARE_METER | m2 | No |
| SQUARE_FOOT | ft2 | No |
| SQUARE_INCH | in2 | ✅ Yes |
| SQUARE_YARD | yd2 | No |

#### Unit

| Name | Symbol | Base Unit |
|------|--------|-----------|
| UNIT | unit | ✅ Yes |
| PIECE | pcs | No |

---

### Utility Functions

```typescript
import {
  findStandardUnitByName,
  findUnitByNameAndCategory,
  findCategoryByUnitName,
  MeasurementUnitCategory,
} from '@olusiji/measurements-converter';

// Find a standard unit definition by name
const unit = findStandardUnitByName('KILOGRAM');
console.log(unit.symbol);           // 'kg'
console.log(unit.conversionFactor); // 1

// Find a unit by name and category
const meter = findUnitByNameAndCategory('METER', MeasurementUnitCategory.LENGTH);
console.log(meter.conversionFactor); // 39.3701

// Look up which category a unit name belongs to
const category = findCategoryByUnitName('GALLON');
console.log(category); // 'VOLUME'

const unknown = findCategoryByUnitName('UNKNOWN');
console.log(unknown); // null
```

---

### Error Handling

The library throws typed errors for invalid operations:

```typescript
import {
  Measurement,
  MeasurementUnit,
  MeasurementUnitCategory,
  InvalidMeasurement,
  InvalidMeasurementUnit,
  MeasurementConversionError
} from '@olusiji/measurements-converter';

// InvalidMeasurementUnit — invalid unit construction
try {
  new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'KILOGRAM', 'kg');
} catch (e) {
  console.log(e instanceof InvalidMeasurementUnit); // true
  console.log(e.message); // 'Invalid name and symbol'
}

// InvalidMeasurement — cross-category conversion
try {
  const meter = new MeasurementUnit(MeasurementUnitCategory.LENGTH, 'METER', 'm');
  const kg    = new MeasurementUnit(MeasurementUnitCategory.WEIGHT, 'KILOGRAM', 'kg');
  new Measurement(meter, 1).convertToMeasurementUnit(kg);
} catch (e) {
  console.log(e instanceof InvalidMeasurement); // true
  console.log(e.message); // 'Cannot convert between different categories'
}
```

## License

MIT