Skip to content

Commit

Permalink
fix: wrong values for floats and datetimes. (#158)
Browse files Browse the repository at this point in the history
* fix: wrong values for floats and datetimes.

* Create eleven-chicken-live.md

* fix
  • Loading branch information
ota-meshi authored Nov 7, 2023
1 parent cb25036 commit 9ab2791
Show file tree
Hide file tree
Showing 13 changed files with 733 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-chicken-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"toml-eslint-parser": patch
---

fix: wrong values for floats and datetimes.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ module.exports = {
selector: "method",
format: null,
},
{
selector: "import",
format: null,
},
],
"no-implicit-globals": "off",
"@typescript-eslint/no-non-null-assertion": "off",
Expand Down
1 change: 1 addition & 0 deletions src/ast/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ export interface BooleanToken extends BaseTOMLToken {
export interface DateTimeToken extends BaseTOMLToken {
type: "OffsetDateTime" | "LocalDateTime" | "LocalDate" | "LocalTime";
value: string;
date: Date;
}
135 changes: 108 additions & 27 deletions src/tokenizer/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ const ESCAPES: Record<number, number> = {
};

type ExponentData = {
left: number;
minus: boolean;
left: number[];
};
type FractionalData = {
minus: boolean;
absInt: number;
absInt: number[];
};
type DateTimeData = {
hasDate: boolean;
Expand All @@ -137,6 +138,9 @@ type DateTimeData = {
hour: number;
minute: number;
second: number;

frac?: number[];
offsetSign?: number;
};

/**
Expand Down Expand Up @@ -283,7 +287,7 @@ export class Tokenizer {
}

private endToken(
type: BareToken["type"] | Comment["type"] | DateTimeToken["type"],
type: BareToken["type"] | Comment["type"],
pos: "start" | "end",
): void;

Expand Down Expand Up @@ -313,6 +317,12 @@ export class Tokenizer {
value: number,
): void;

private endToken(
type: DateTimeToken["type"],
pos: "start" | "end",
value: Date,
): void;

private endToken(
type: BooleanToken["type"],
pos: "start" | "end",
Expand All @@ -325,7 +335,7 @@ export class Tokenizer {
private endToken(
type: TokenType | Comment["type"],
pos: "start" | "end",
option1?: number[] | number | boolean,
option1?: number[] | number | boolean | Date,
option2?: 16 | 10 | 8 | 2,
): void {
const { tokenStart } = this;
Expand Down Expand Up @@ -396,6 +406,19 @@ export class Tokenizer {
range,
loc,
};
} else if (
type === "LocalDate" ||
type === "LocalTime" ||
type === "LocalDateTime" ||
type === "OffsetDateTime"
) {
token = {
type,
value,
date: option1! as Date,
range,
loc,
};
} else {
token = {
type,
Expand Down Expand Up @@ -764,15 +787,16 @@ export class Tokenizer {
if (cp === LATIN_SMALL_E || cp === LATIN_CAPITAL_E) {
const data: ExponentData = {
// Float values -0.0 and +0.0 are valid and should map according to IEEE 754.
left: sign === DASH ? -0 : 0,
minus: sign === DASH,
left: [DIGIT_0],
};
this.data = data;
return "EXPONENT_RIGHT";
}
if (cp === DOT) {
const data: FractionalData = {
minus: sign === DASH,
absInt: 0,
absInt: [DIGIT_0],
};
this.data = data;
return "FRACTIONAL_RIGHT";
Expand Down Expand Up @@ -821,17 +845,17 @@ export class Tokenizer {
}

if (nextCp === LATIN_SMALL_E || nextCp === LATIN_CAPITAL_E) {
const absNum = Number(String.fromCodePoint(...codePoints));
const data: ExponentData = {
left: sign === DASH ? -absNum : absNum,
minus: sign === DASH,
left: codePoints,
};
this.data = data;
return "EXPONENT_RIGHT";
}
if (nextCp === DOT) {
const data: FractionalData = {
minus: sign === DASH,
absInt: Number(String.fromCodePoint(...codePoints)),
absInt: codePoints,
};
this.data = data;
return "FRACTIONAL_RIGHT";
Expand Down Expand Up @@ -869,34 +893,42 @@ export class Tokenizer {
private FRACTIONAL_RIGHT(cp: number): TokenizerState {
const { minus, absInt } = this.data! as FractionalData;
const { codePoints, nextCp } = this.parseDigits(cp, isDigit);
const absNum =
absInt +
Number(String.fromCodePoint(...codePoints)) *
Math.pow(10, -codePoints.length);
const absNum = [...absInt, DOT, ...codePoints];
if (nextCp === LATIN_SMALL_E || nextCp === LATIN_CAPITAL_E) {
const data: ExponentData = {
left: minus ? -absNum : absNum,
minus,
left: absNum,
};
this.data = data;
return "EXPONENT_RIGHT";
}
this.endToken("Float", "start", minus ? -absNum : absNum);
const value = Number(
minus
? String.fromCodePoint(DASH, ...absNum)
: String.fromCodePoint(...absNum),
);
this.endToken("Float", "start", value);
return this.back("DATA");
}

private EXPONENT_RIGHT(cp: number): TokenizerState {
const { left } = this.data! as ExponentData;
const { left, minus: leftMinus } = this.data! as ExponentData;
let minus = false;
if (cp === DASH || cp === PLUS_SIGN) {
minus = cp === DASH;
cp = this.nextCode();
}
const { codePoints } = this.parseDigits(cp, isDigit);
let right = Number(String.fromCodePoint(...codePoints));
let right = codePoints;
if (minus) {
right = 0 - right;
right = [DASH, ...right];
}
this.endToken("Float", "start", left * Math.pow(10, right));
const value = Number(
leftMinus
? String.fromCodePoint(DASH, ...left, LATIN_SMALL_E, ...right)
: String.fromCodePoint(...left, LATIN_SMALL_E, ...right),
);
this.endToken("Float", "start", value);
return this.back("DATA");
}

Expand Down Expand Up @@ -990,7 +1022,8 @@ export class Tokenizer {
return "TIME_HOUR";
}
}
this.endToken("LocalDate", "start");
const dateValue = getDateFromDateTimeData(data, "Z");
this.endToken("LocalDate", "start", dateValue);
return this.back("DATA");
}

Expand Down Expand Up @@ -1063,39 +1096,50 @@ export class Tokenizer {
}
if (data.hasDate) {
if (cp === DASH || cp === PLUS_SIGN) {
data.offsetSign = cp;
return "TIME_OFFSET";
}
if (cp === LATIN_CAPITAL_Z || cp === LATIN_SMALL_Z) {
this.endToken("OffsetDateTime", "end");
const dateValue = getDateFromDateTimeData(data, "Z");
this.endToken("OffsetDateTime", "end", dateValue);
return "DATA";
}
this.endToken("LocalDateTime", "start");
const dateValue = getDateFromDateTimeData(data, "");
this.endToken("LocalDateTime", "start", dateValue);
return this.back("DATA");
}
this.endToken("LocalTime", "start");
const dateValue = getDateFromDateTimeData(data, "Z");
this.endToken("LocalTime", "start", dateValue);
return this.back("DATA");
}

private TIME_SEC_FRAC(cp: number): TokenizerState {
if (!isDigit(cp)) {
return this.reportParseError("unexpected-char");
}
const codePoints = [];
while (isDigit(cp)) {
codePoints.push(cp);
cp = this.nextCode();
}
const data: DateTimeData = this.data! as DateTimeData;
data.frac = codePoints;
if (data.hasDate) {
if (cp === DASH || cp === PLUS_SIGN) {
data.offsetSign = cp;
return "TIME_OFFSET";
}
if (cp === LATIN_CAPITAL_Z || cp === LATIN_SMALL_Z) {
this.endToken("OffsetDateTime", "end");
const dateValue = getDateFromDateTimeData(data, "Z");
this.endToken("OffsetDateTime", "end", dateValue);
return "DATA";
}
this.endToken("LocalDateTime", "start");
const dateValue = getDateFromDateTimeData(data, "");
this.endToken("LocalDateTime", "start", dateValue);
return this.back("DATA");
}
this.endToken("LocalTime", "start");
const dateValue = getDateFromDateTimeData(data, "Z");
this.endToken("LocalTime", "start", dateValue);
return this.back("DATA");
}

Expand Down Expand Up @@ -1133,7 +1177,15 @@ export class Tokenizer {
return this.reportParseError("invalid-time");
}

this.endToken("OffsetDateTime", "end");
const data: DateTimeData = this.data! as DateTimeData;
const dateValue = getDateFromDateTimeData(
data,
`${String.fromCodePoint(data.offsetSign!)}${padStart(hour, 2)}:${padStart(
minute,
2,
)}`,
);
this.endToken("OffsetDateTime", "end", dateValue);
return "DATA";
}

Expand Down Expand Up @@ -1233,3 +1285,32 @@ function isValidTime(h: number, m: number, s: number): boolean {
}
return true;
}

/**
* Get date from DateTimeData
*/
function getDateFromDateTimeData(data: DateTimeData, timeZone: string): Date {
const year = padStart(data.year, 4);
const month = data.month ? padStart(data.month, 2) : "01";
const day = data.day ? padStart(data.day, 2) : "01";
const hour = padStart(data.hour, 2);
const minute = padStart(data.minute, 2);
const second = padStart(data.second, 2);
const textDate = `${year}-${month}-${day}`;
const frac = data.frac ? `.${String.fromCodePoint(...data.frac)}` : "";
const dateValue = new Date(
`${textDate}T${hour}:${minute}:${second}${frac}${timeZone}`,
);
if (!isNaN(dateValue.getTime()) || data.second !== 60) {
return dateValue;
}
// leap seconds?
return new Date(`${textDate}T${hour}:${minute}:59${frac}${timeZone}`);
}

/**
* Pad with zeros.
*/
function padStart(num: number, maxLength: number): string {
return String(num).padStart(maxLength, "0");
}
10 changes: 1 addition & 9 deletions src/toml-parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,18 +411,10 @@ export class TOMLParser {
ctx: Context,
): ParserState[] {
const valueContainer = ctx.consumeValueContainer();
let textDate =
token.type !== "LocalTime" ? token.value : `0000-01-01T${token.value}Z`;
let dateValue = new Date(textDate);
if (isNaN(dateValue.getTime())) {
// leap seconds?
textDate = textDate.replace(/(\d{2}:\d{2}):60/u, "$1:59");
dateValue = new Date(textDate);
}
const node: TOMLDateTimeValue = {
type: "TOMLValue",
kind: DATETIME_VALUE_KIND_MAP[token.type],
value: dateValue,
value: token.date,
datetime: token.value,
parent: valueContainer.parent,
range: cloneRange(token.range),
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/parser/ast/date-time03-min-input.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
first-offset = 0001-01-01 00:00:00Z
first-local = 0001-01-01 00:00:00
first-date = 0001-01-01
Loading

0 comments on commit 9ab2791

Please sign in to comment.