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
13 changes: 11 additions & 2 deletions packages/quicktype-core/src/DateTime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/* eslint-disable */
// Adapted from ajv:
// https://github.com/epoberezkin/ajv/blob/4d76c6fb813b136b6ec4fe74990bc97233d75dea/lib/compile/formats.js
//
// We deviate from ajv in that we recognize only strict RFC 3339 date-times:
// the date/time separator must be "T" (or "t"), not a space, and times must
// carry a timezone offset ("Z"/"z" or "+hh:mm"/"-hh:mm"). ajv accepted the
// lenient forms, which meant we inferred "date-time" for strings like
// "2013-06-15 21:10:28" that most target languages' date parsers reject.

/*
The MIT License (MIT)
Expand Down Expand Up @@ -27,15 +34,17 @@ SOFTWARE.

const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
const DAYS = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)?$/i;
// RFC 3339 full-time: the timezone offset is mandatory.
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)$/i;

export interface DateTimeRecognizer {
isDate: (s: string) => boolean;
isDateTime: (s: string) => boolean;
isTime: (s: string) => boolean;
}

const DATE_TIME_SEPARATOR = /t|\s/i;
// RFC 3339 date-time: the separator must be "T" or "t", never a space.
const DATE_TIME_SEPARATOR = /t/i;

export class DefaultDateTimeRecognizer implements DateTimeRecognizer {
isDate(str: string) {
Expand Down
36 changes: 2 additions & 34 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const CSharpLanguage: Language = {
return `dotnet run -p:CheckEolTargetFramework=false -- "${sample}"`;
},
diffViaSchema: true,
skipDiffViaSchema: ["34702.json", "437e7.json"],
skipDiffViaSchema: ["34702.json"],
allowMissingNull: false,
features: [
"enum",
Expand Down Expand Up @@ -134,7 +134,7 @@ export const CSharpLanguageSystemTextJson: Language = {
return `dotnet run -p:CheckEolTargetFramework=false -- "${sample}"`;
},
diffViaSchema: true,
skipDiffViaSchema: ["34702.json", "437e7.json"],
skipDiffViaSchema: ["34702.json"],
allowMissingNull: false,
features: [
"enum",
Expand Down Expand Up @@ -469,15 +469,6 @@ export const GoLanguage: Language = {
// can't differenciate empty array and nothing for optional empty array
// (omitempty).
"github-events.json",
// files contains datetime filed with contain non rfc3339 format value
"f6a65.json",
"e0ac7.json",
"c3303.json",
"7681c.json",
"437e7.json",
"127a1.json",
"26b49.json",
"0cffa.json",
],
skipMiscJSON: false,
skipSchema: [
Expand Down Expand Up @@ -592,15 +583,9 @@ export const CPlusPlusLanguage: Language = {
"bug427.json",
"keywords.json",
"0a91a.json",
"0cffa.json",
"127a1.json",
"26b49.json",
"34702.json",
"7681c.json",
"7f568.json",
"c3303.json",
"e8b04.json",
"f6a65.json",
"fcca3.json",
],
allowMissingNull: false,
Expand Down Expand Up @@ -663,9 +648,7 @@ export const ElmLanguage: Language = {
"github-events.json",
"nbl-stats.json",
"0a91a.json",
"0cffa.json",
"0e0c2.json",
"127a1.json",
"29f47.json",
"2df80.json",
"27332.json",
Expand All @@ -676,13 +659,10 @@ export const ElmLanguage: Language = {
"6de06.json",
"76ae1.json",
"7eb30.json",
"7681c.json",
"ae9ca.json",
"af2d1.json",
"be234.json",
"c3303.json",
"e8b04.json",
"f6a65.json",
],
allowMissingNull: false,
features: ["enum", "union", "no-defaults"],
Expand Down Expand Up @@ -731,17 +711,12 @@ export const SwiftLanguage: Language = {
"keywords.json",
"0a358.json", // date-time issues
"0a91a.json",
"0cffa.json", // date-time issues
"127a1.json", // date-time issues
"26b49.json", // date-time issues
"26c9c.json", // uri/string confusion
"32d5c.json", // date-time issues
"337ed.json",
"34702.json",
"437e7.json", // date-time issues
"54d32.json", // date-time issues
"5eae5.json", // date-time issues
"7681c.json", // date-time issues
"77392.json", // date-time issues
"7f568.json",
"734ad.json",
Expand All @@ -750,13 +725,10 @@ export const SwiftLanguage: Language = {
"9ac3b.json", // date-time issues
"a0496.json", // date-time issues
"b4865.json", // date-time issues
"c3303.json", // date-time issues
"c8c7e.json",
"d23d5.json", // date-time issues
"e0ac7.json", // date-time issues
"e53b5.json",
"e8b04.json",
"f6a65.json", // date-time issues
"fcca3.json",
"f82d9.json",
"bug863.json", // Unable to resolve reserved keyword use, "description"
Expand Down Expand Up @@ -872,7 +844,6 @@ export const TypeScriptLanguage: Language = {
output: "TopLevel.ts",
topLevel: "TopLevel",
skipJSON: [
"7681c.json", // year 0 is out of range
],
skipMiscJSON: false,
skipSchema: ["keyword-unions.schema"], // can't handle "constructor" property
Expand Down Expand Up @@ -904,7 +875,6 @@ export const JavaScriptLanguage: Language = {
output: "TopLevel.js",
topLevel: "TopLevel",
skipJSON: [
"7681c.json", // year 0 is out of range
],
skipMiscJSON: false,
skipSchema: ["keyword-unions.schema"], // can't handle "constructor" property
Expand Down Expand Up @@ -962,7 +932,6 @@ export const FlowLanguage: Language = {
output: "TopLevel.js",
topLevel: "TopLevel",
skipJSON: [
"7681c.json", // year 0 is out of range
],
skipMiscJSON: false,
skipSchema: [
Expand Down Expand Up @@ -1612,7 +1581,6 @@ export const TypeScriptZodLanguage: Language = {
"nst-test-suite.json",
"keywords.json",
"ed095.json",
"7681c.json",
"32d5c.json",
],
skipMiscJSON: false,
Expand Down
85 changes: 85 additions & 0 deletions test/unit/date-time-recognizer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// The default date/time recognizer decides whether a JSON string sample is
// inferred as "date", "time", or "date-time". It must accept only strict
// RFC 3339 forms: "date-time" requires the "T" (or "t") separator — a space
// is not allowed — and times must carry a timezone offset. Before this was
// tightened, strings like "2013-06-15 21:10:28" were inferred as date-times,
// producing generated code whose strict date parsers (Go, Swift, java.time,
// ...) could not read the very samples the types were inferred from.
import { DefaultDateTimeRecognizer } from "quicktype-core/dist/DateTime.js";
import { describe, expect, test } from "vitest";

const recognizer = new DefaultDateTimeRecognizer();

describe("isDateTime", () => {
test("accepts RFC 3339 date-times with offset", () => {
expect(recognizer.isDateTime("2018-08-14T02:45:50Z")).toBe(true);
expect(recognizer.isDateTime("2018-08-14T02:45:50+01:00")).toBe(true);
expect(recognizer.isDateTime("2018-08-14T02:45:50-11:30")).toBe(true);
});

test("accepts lowercase t and z", () => {
expect(recognizer.isDateTime("2018-08-14t02:45:50z")).toBe(true);
});

test("accepts fractional seconds", () => {
expect(recognizer.isDateTime("2015-04-24T01:46:50.342496Z")).toBe(true);
expect(recognizer.isDateTime("2010-01-12T00:00:00.000Z")).toBe(true);
expect(recognizer.isDateTime("2015-07-15T23:25:21.541+02:00")).toBe(
true,
);
});

test("rejects a space separator", () => {
expect(recognizer.isDateTime("2013-06-15 21:10:28")).toBe(false);
expect(recognizer.isDateTime("1970-01-01 00:00:00")).toBe(false);
expect(recognizer.isDateTime("2015-04-24 01:46:50.342496Z")).toBe(
false,
);
});

test("rejects a missing timezone offset", () => {
expect(recognizer.isDateTime("2018-08-14T02:45:50")).toBe(false);
expect(recognizer.isDateTime("2018-08-14T02:45:50.123")).toBe(false);
});

test("rejects invalid dates and times", () => {
expect(recognizer.isDateTime("0000-00-00T00:00:00Z")).toBe(false);
expect(recognizer.isDateTime("2018-13-01T00:00:00Z")).toBe(false);
expect(recognizer.isDateTime("2018-08-14T24:00:00Z")).toBe(false);
expect(recognizer.isDateTime("2018-08-14X02:45:50Z")).toBe(false);
});
});

describe("isDate", () => {
test("accepts RFC 3339 full-date", () => {
expect(recognizer.isDate("2018-08-14")).toBe(true);
expect(recognizer.isDate("1970-01-01")).toBe(true);
});

test("rejects invalid dates", () => {
expect(recognizer.isDate("0000-00-00")).toBe(false);
expect(recognizer.isDate("2018-13-01")).toBe(false);
expect(recognizer.isDate("2018-02-30")).toBe(false);
expect(recognizer.isDate("2018-8-14")).toBe(false);
});
});

describe("isTime", () => {
test("accepts RFC 3339 full-time (offset required)", () => {
expect(recognizer.isTime("02:45:50Z")).toBe(true);
expect(recognizer.isTime("02:45:50z")).toBe(true);
expect(recognizer.isTime("02:45:50.123+01:00")).toBe(true);
expect(recognizer.isTime("23:59:59-11:30")).toBe(true);
});

test("rejects a missing timezone offset", () => {
expect(recognizer.isTime("02:45:50")).toBe(false);
expect(recognizer.isTime("02:45:50.123")).toBe(false);
});

test("rejects invalid times", () => {
expect(recognizer.isTime("24:00:00Z")).toBe(false);
expect(recognizer.isTime("02:60:00Z")).toBe(false);
expect(recognizer.isTime("02:45:60Z")).toBe(false);
});
});
Loading