Skip to content

Commit 067a398

Browse files
committed
feat: support "m" as an alias for minutes
Closes #659
1 parent 909549a commit 067a398

File tree

6 files changed

+64
-12
lines changed

6 files changed

+64
-12
lines changed

docs/api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type BestUnits<T extends BestKind = BestKind> = BestUnits_2[T];
2929
export type BestUnitsForMeasure<M extends MeasureKind, K extends BestKind = BestKind> = BestUnitsForUnit<UnitsByMeasure<M>, K>;
3030

3131
// @public
32-
export type BestUnitsForUnit<U extends Unit, K extends BestKind = BestKind> = U & BestUnits<K>;
32+
export type BestUnitsForUnit<U extends Unit, K extends BestKind = BestKind> = (Time | Length extends U ? Exclude<U, Time> : Time | 'm' extends U ? Exclude<U, 'm'> : U) & BestUnits<K>;
3333

3434
// @public
3535
function convert<Q extends number | bigint, U extends Unit>(quantity: Q, from: U): Converter<Q, MeasuresByUnit<U>>;
@@ -99,7 +99,7 @@ export type _MeasureKindByUnit<T extends Unit> = {
9999
}[MeasureKind];
100100

101101
// @public
102-
export type MeasuresByUnit<T extends Unit> = UnitsByMeasure<_MeasureKindByUnit<T>>;
102+
export type MeasuresByUnit<T extends Unit> = UnitsByMeasure<_MeasureKindByUnit<T>> | (T extends 'm' ? Time : never) | (T extends Time ? 'm' : never);
103103

104104
// Warning: (ae-incompatible-release-tags) The symbol "ms" is marked as @public, but its signature references "_LiteralToPrimitive" which is marked as @internal
105105
//

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,22 @@
7777
"size-limit": [
7878
{
7979
"brotli": true,
80-
"limit": "5.42 KB",
80+
"limit": "5.46 KB",
8181
"path": "./dist/index.js"
8282
},
8383
{
8484
"gzip": true,
85-
"limit": "6.83 KB",
85+
"limit": "6.88 KB",
8686
"path": "./dist/index.js"
8787
},
8888
{
8989
"brotli": true,
90-
"limit": "5.20 KB",
90+
"limit": "5.22 KB",
9191
"path": "./dist/index.mjs"
9292
},
9393
{
9494
"gzip": true,
95-
"limit": "6.59 KB",
95+
"limit": "6.64 KB",
9696
"path": "./dist/index.mjs"
9797
}
9898
]
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect } from 'vitest';
2+
import { assertConversions } from '../../../test/assert-conversion';
3+
import type { Length, Time } from '../../types/units';
4+
import { convert } from '../convert';
5+
6+
describe('special case for m', () => {
7+
assertConversions([{ from: [1, 'm'], to: [1, 'm'] }]);
8+
9+
assertConversions([{ from: [1, 'm'], to: [60, 's'] }]);
10+
assertConversions([{ from: [60, 's'], to: [1, 'm'] }]);
11+
assertConversions([{ from: [1, 'm'], to: [1 / 1000, 'km'] }]);
12+
13+
// Compilation tests
14+
convert(1, 'm').to('s');
15+
convert(60, 's').to('m');
16+
17+
const best1m: `${number}${Length}` = convert(1, 'm').to('best').toString();
18+
const best60s: `${number}${Time}` = convert(1, 'min').to('best').toString();
19+
20+
expect(best1m).toBe('1m');
21+
expect(best60s).toBe('60s');
22+
});

src/converters/convert.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
1-
import type { BestKind } from '../conversions/types';
1+
import { type BestKind, type MeasureKind } from '../conversions/types';
22
import { bestUnits } from '../generated/best-units';
33
import { differences, unitsObject } from '../generated/parse-unit';
44
import type { BestConversion, Converter } from '../types/converter';
55
import type { BestUnitsForUnit, MeasuresByUnit, Unit } from '../types/units';
66
import type { LiteralToPrimitive } from '../types/utils';
77

8+
// Importing MeasureKind will cause the entire enum to be included in the output, which increases bundle size
9+
// This workaround allows me to just hardcode the value, but ensures it doesn't become inaccurate if the enum changes
10+
const MeasureKindTime: MeasureKind.Time = 10;
11+
12+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: No easy way to reduce complexity
813
function convertTo<Q extends number | bigint>(from: string, quantity: Q, to: string): LiteralToPrimitive<Q> {
9-
const parsedTo = unitsObject[to as Unit];
14+
let parsedTo = unitsObject[to as Unit];
1015

1116
if (!parsedTo) {
1217
throw new RangeError(`${to} is not a valid unit`);
1318
}
1419

15-
const parsedFrom = unitsObject[from as Unit];
20+
let parsedFrom = unitsObject[from as Unit];
1621

1722
if (parsedFrom[0] !== parsedTo[0]) {
18-
throw new RangeError(`Cannot convert between different measures: ${from} and ${to}`);
23+
if (parsedFrom[0] === MeasureKindTime && to === 'm') {
24+
// Going from time to meters, you meant to do minutes
25+
parsedTo = unitsObject.min;
26+
} else if (from === 'm' && parsedTo[0] === MeasureKindTime) {
27+
parsedFrom = unitsObject.min;
28+
} else {
29+
throw new RangeError(`Cannot convert between different measures: ${from} and ${to}`);
30+
}
1931
}
2032

2133
const fromRatio = parsedFrom[1];

src/converters/ms.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('conversions', () => {
55
describe('duration string to ms', () => {
66
test.each([
77
['1s', 1000],
8+
['1m', 60_000],
89
['1d', 86_400_000],
910
['1d 24h', 86_400_000 * 2],
1011
])('%p -> %p', (from, to) => {

src/types/units.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ export type MeasureKindByUnit<T extends Unit> = {
1919
* A type that gives all the units compatible with the same measure as a given unit.
2020
* @public
2121
*/
22-
export type MeasuresByUnit<T extends Unit> = UnitsByMeasure<MeasureKindByUnit<T>>;
22+
export type MeasuresByUnit<T extends Unit> =
23+
| UnitsByMeasure<MeasureKindByUnit<T>>
24+
// Special case for converting from 'm' (alias for minutes) to time
25+
| (T extends 'm' ? Time : never)
26+
// Special case for converting from time to 'm' (alias for minutes)
27+
| (T extends Time ? 'm' : never);
2328

2429
/**
2530
* A supported unit you can convert.
@@ -46,7 +51,19 @@ export type BestUnitsForMeasure<M extends MeasureKind, K extends BestKind = Best
4651
* Get the best units for a given unit.
4752
* @public
4853
*/
49-
export type BestUnitsForUnit<U extends Unit, K extends BestKind = BestKind> = U & BestUnits<K>;
54+
// We have special logic related to the 'm' unit, where it is injected in some cases
55+
// Or when Time is injected if you are converting from 'm'
56+
// We need to undo that logic here, otherwise the best units will be incorrect for 'm' or Time
57+
// Cases:
58+
// 1. U = Time | Length, output should be U - Time
59+
// 2. U = Time | 'm', output should be U - 'm'
60+
// 3. U = something else, output should be U (no transformation)
61+
export type BestUnitsForUnit<U extends Unit, K extends BestKind = BestKind> = (Time | Length extends U
62+
? Exclude<U, Time>
63+
: Time | 'm' extends U
64+
? Exclude<U, 'm'>
65+
: U) &
66+
BestUnits<K>;
5067

5168
/**
5269
* Valid angle units.

0 commit comments

Comments
 (0)