Skip to content

Commit 3f20258

Browse files
lxcidcursoragent
andauthored
fix: add support for ISO 8601 basic format and bare hours in parseTimeString (#17)
Add support for two new time input formats: 1. ISO 8601 basic format (compact, no colons): - 4-digit format (hhmm): "1430" → 14:30, "0900" → 09:00 - 6-digit format (hhmmss): "143045" → 14:30:45 - With fractional seconds: "143045.123" → 14:30:45.123 2. Bare hours (defaults to :00 minutes): - 24-hour format: "7" → 7:00, "0" → 0:00, "23" → 23:00 - 12-hour format: "7 AM" → 7:00 AM, "12 PM" → 12:00 PM The parser intelligently detects the format based on digit count and presence of colons: - 4 or 6 digits without colon → ISO 8601 basic format - 1-2 digits without colon → bare hour - With colon → extended format (existing behavior) This makes the parser more flexible for parsing time strings from various sources while maintaining full backward compatibility with existing formats. Test coverage: Added 20 new tests (11 for bare hours, 14 for ISO 8601 basic format) with comprehensive validation of edge cases. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b388f11 commit 3f20258

3 files changed

Lines changed: 381 additions & 44 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@taskade/temporal-parser": patch
3+
---
4+
5+
Add support for ISO 8601 basic format and bare hours in `parseTimeString()`
6+
7+
**New formats supported:**
8+
9+
1. **ISO 8601 basic format** (compact, no colons):
10+
- `"1430"` → 14:30
11+
- `"143045"` → 14:30:45
12+
- `"143045.123"` → 14:30:45.123
13+
14+
2. **Bare hours** (defaults to :00 minutes):
15+
- `"7"` → 7:00
16+
- `"7 AM"` → 7:00 AM
17+
- `"23"` → 23:00
18+
19+
The parser intelligently detects the format based on digit count and presence of colons, maintaining full backward compatibility with existing formats.

src/parseTimeString.test.ts

Lines changed: 288 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -340,18 +340,15 @@ describe('parseTimeString', () => {
340340
}
341341
});
342342

343-
it('should throw on missing colon with format hint', () => {
344-
try {
345-
parseTimeString('1430');
346-
expect.fail('Should have thrown');
347-
} catch (e) {
348-
expect(e).toBeInstanceOf(ParseError);
349-
const message = (e as ParseError).message;
350-
expect(message).toBe(
351-
'Invalid time format: "1430". Expected format: "HH:MM" (e.g., "09:00", "14:30") or "H:MM AM/PM" (e.g., "9:07 AM") at token index 1',
352-
);
353-
console.log('Missing colon error:', message);
354-
}
343+
it('should parse ISO 8601 basic format: "1430"', () => {
344+
const result = parseTimeString('1430');
345+
expect(result).toEqual({
346+
kind: 'Time',
347+
hour: 14,
348+
minute: 30,
349+
second: undefined,
350+
fraction: undefined,
351+
});
355352
});
356353

357354
it('should throw on invalid hour in 24-hour format with helpful message', () => {
@@ -696,4 +693,283 @@ describe('parseTimeString', () => {
696693
});
697694
});
698695
});
696+
697+
describe('bare single-digit hours', () => {
698+
it('should parse single digit "0" as 00:00', () => {
699+
const result = parseTimeString('0');
700+
expect(result).toEqual({
701+
kind: 'Time',
702+
hour: 0,
703+
minute: 0,
704+
second: undefined,
705+
fraction: undefined,
706+
});
707+
});
708+
709+
it('should parse single digit "7" as 07:00', () => {
710+
const result = parseTimeString('7');
711+
expect(result).toEqual({
712+
kind: 'Time',
713+
hour: 7,
714+
minute: 0,
715+
second: undefined,
716+
fraction: undefined,
717+
});
718+
});
719+
720+
it('should parse single digit "9" as 09:00', () => {
721+
const result = parseTimeString('9');
722+
expect(result).toEqual({
723+
kind: 'Time',
724+
hour: 9,
725+
minute: 0,
726+
second: undefined,
727+
fraction: undefined,
728+
});
729+
});
730+
731+
it('should parse double digit "23" as 23:00', () => {
732+
const result = parseTimeString('23');
733+
expect(result).toEqual({
734+
kind: 'Time',
735+
hour: 23,
736+
minute: 0,
737+
second: undefined,
738+
fraction: undefined,
739+
});
740+
});
741+
742+
it('should parse "7 AM" as 7:00 AM', () => {
743+
const result = parseTimeString('7 AM');
744+
expect(result).toEqual({
745+
kind: 'Time',
746+
hour: 7,
747+
minute: 0,
748+
second: undefined,
749+
fraction: undefined,
750+
});
751+
});
752+
753+
it('should parse "12 PM" as 12:00 PM (noon)', () => {
754+
const result = parseTimeString('12 PM');
755+
expect(result).toEqual({
756+
kind: 'Time',
757+
hour: 12,
758+
minute: 0,
759+
second: undefined,
760+
fraction: undefined,
761+
});
762+
});
763+
764+
it('should parse "12 AM" as 12:00 AM (midnight)', () => {
765+
const result = parseTimeString('12 AM');
766+
expect(result).toEqual({
767+
kind: 'Time',
768+
hour: 0,
769+
minute: 0,
770+
second: undefined,
771+
fraction: undefined,
772+
});
773+
});
774+
775+
it('should parse "3 pm" (lowercase) as 15:00', () => {
776+
const result = parseTimeString('3 pm');
777+
expect(result).toEqual({
778+
kind: 'Time',
779+
hour: 15,
780+
minute: 0,
781+
second: undefined,
782+
fraction: undefined,
783+
});
784+
});
785+
786+
it('should throw on "0 AM" (invalid 12-hour format)', () => {
787+
try {
788+
parseTimeString('0 AM');
789+
expect.fail('Should have thrown');
790+
} catch (e) {
791+
expect(e).toBeInstanceOf(ParseError);
792+
}
793+
});
794+
795+
it('should throw on "24" (invalid 24-hour format)', () => {
796+
try {
797+
parseTimeString('24');
798+
expect.fail('Should have thrown');
799+
} catch (e) {
800+
expect(e).toBeInstanceOf(ParseError);
801+
const message = (e as ParseError).message;
802+
expect(message).toContain('Invalid hour for 24-hour format: 24');
803+
}
804+
});
805+
806+
it('should throw on "13 AM" (invalid 12-hour format)', () => {
807+
try {
808+
parseTimeString('13 AM');
809+
expect.fail('Should have thrown');
810+
} catch (e) {
811+
expect(e).toBeInstanceOf(ParseError);
812+
const message = (e as ParseError).message;
813+
expect(message).toContain('Invalid hour for 12-hour format: 13');
814+
}
815+
});
816+
});
817+
818+
describe('ISO 8601 basic format (compact, no colons)', () => {
819+
it('should parse "1430" as 14:30', () => {
820+
const result = parseTimeString('1430');
821+
expect(result).toEqual({
822+
kind: 'Time',
823+
hour: 14,
824+
minute: 30,
825+
second: undefined,
826+
fraction: undefined,
827+
});
828+
});
829+
830+
it('should parse "0900" as 09:00', () => {
831+
const result = parseTimeString('0900');
832+
expect(result).toEqual({
833+
kind: 'Time',
834+
hour: 9,
835+
minute: 0,
836+
second: undefined,
837+
fraction: undefined,
838+
});
839+
});
840+
841+
it('should parse "0000" as 00:00 (midnight)', () => {
842+
const result = parseTimeString('0000');
843+
expect(result).toEqual({
844+
kind: 'Time',
845+
hour: 0,
846+
minute: 0,
847+
second: undefined,
848+
fraction: undefined,
849+
});
850+
});
851+
852+
it('should parse "2359" as 23:59', () => {
853+
const result = parseTimeString('2359');
854+
expect(result).toEqual({
855+
kind: 'Time',
856+
hour: 23,
857+
minute: 59,
858+
second: undefined,
859+
fraction: undefined,
860+
});
861+
});
862+
863+
it('should parse "143045" as 14:30:45 (with seconds)', () => {
864+
const result = parseTimeString('143045');
865+
expect(result).toEqual({
866+
kind: 'Time',
867+
hour: 14,
868+
minute: 30,
869+
second: 45,
870+
fraction: undefined,
871+
});
872+
});
873+
874+
it('should parse "090000" as 09:00:00', () => {
875+
const result = parseTimeString('090000');
876+
expect(result).toEqual({
877+
kind: 'Time',
878+
hour: 9,
879+
minute: 0,
880+
second: 0,
881+
fraction: undefined,
882+
});
883+
});
884+
885+
it('should parse "235959" as 23:59:59', () => {
886+
const result = parseTimeString('235959');
887+
expect(result).toEqual({
888+
kind: 'Time',
889+
hour: 23,
890+
minute: 59,
891+
second: 59,
892+
fraction: undefined,
893+
});
894+
});
895+
896+
it('should parse "143045.123" with fractional seconds', () => {
897+
const result = parseTimeString('143045.123');
898+
expect(result).toEqual({
899+
kind: 'Time',
900+
hour: 14,
901+
minute: 30,
902+
second: 45,
903+
fraction: '123',
904+
});
905+
});
906+
907+
it('should parse "143045,123" with comma separator', () => {
908+
const result = parseTimeString('143045,123');
909+
expect(result).toEqual({
910+
kind: 'Time',
911+
hour: 14,
912+
minute: 30,
913+
second: 45,
914+
fraction: '123',
915+
});
916+
});
917+
918+
it('should throw on invalid basic format hour "2430"', () => {
919+
try {
920+
parseTimeString('2430');
921+
expect.fail('Should have thrown');
922+
} catch (e) {
923+
expect(e).toBeInstanceOf(ParseError);
924+
const message = (e as ParseError).message;
925+
expect(message).toContain('Invalid hour for 24-hour format: 24');
926+
}
927+
});
928+
929+
it('should throw on invalid basic format minute "1460"', () => {
930+
try {
931+
parseTimeString('1460');
932+
expect.fail('Should have thrown');
933+
} catch (e) {
934+
expect(e).toBeInstanceOf(ParseError);
935+
const message = (e as ParseError).message;
936+
expect(message).toContain('Invalid minute: 60');
937+
}
938+
});
939+
940+
it('should throw on invalid basic format second "143060"', () => {
941+
try {
942+
parseTimeString('143060');
943+
expect.fail('Should have thrown');
944+
} catch (e) {
945+
expect(e).toBeInstanceOf(ParseError);
946+
const message = (e as ParseError).message;
947+
expect(message).toContain('Invalid second: 60');
948+
}
949+
});
950+
951+
it('should not confuse 3-digit number "123" with basic format', () => {
952+
// "123" should be treated as bare hour 123, which will fail validation
953+
try {
954+
parseTimeString('123');
955+
expect.fail('Should have thrown');
956+
} catch (e) {
957+
expect(e).toBeInstanceOf(ParseError);
958+
const message = (e as ParseError).message;
959+
expect(message).toContain('Invalid hour for 24-hour format: 123');
960+
}
961+
});
962+
963+
it('should not confuse 5-digit number "12345" with basic format', () => {
964+
// "12345" should be treated as bare hour 12345, which will fail validation
965+
try {
966+
parseTimeString('12345');
967+
expect.fail('Should have thrown');
968+
} catch (e) {
969+
expect(e).toBeInstanceOf(ParseError);
970+
const message = (e as ParseError).message;
971+
expect(message).toContain('Invalid hour for 24-hour format: 12345');
972+
}
973+
});
974+
});
699975
});

0 commit comments

Comments
 (0)