Skip to content

Commit

Permalink
Add support to modifiers after snap operators
Browse files Browse the repository at this point in the history
Prior to this commit no further amount modifiers
could be appended after snap operations (E.g:
"now/d+1h"). This commit adds support for chaining
modifiers with no limits.

Regexes to parse content have been dropped in favor
of two new elements: a lexer and a Pratt parser. Token
are not as complex as a language, so recursion is
not used as ast nodes can be stored in a sequential
plain list.

Some other features have been dropped as they were
rather redudant. Should the number of amount modifiers
be limited by the end user, the library should remain
agnostic about.

Features dropped:
    - `complex_token_to_date` util
    - `simple_token_to_date` util
    - `SimpleToken` model
    - `ComplexToken` model
  • Loading branch information
sonirico committed Jan 17, 2019
1 parent af4b4c6 commit 0b9adf0
Show file tree
Hide file tree
Showing 70 changed files with 2,058 additions and 689 deletions.
38 changes: 38 additions & 0 deletions ast/ast.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Token } from '../token';
export interface Expression {
token: Token;
operate(date?: Date): Date;
toString(): string;
}
export declare class NowExpression implements Expression {
token: Token;
constructor(token: Token);
operate(date: Date): Date;
toString(): string;
}
export declare class ModifierExpression implements Expression {
token: Token;
amount: number;
operator: string;
modifier: string;
constructor(token: Token, amount: number | undefined, operator: string, modifier: string);
operate(date: Date): Date;
toString(): string;
}
export declare class SnapExpression implements Expression {
token: Token;
modifier: string;
operator: string;
constructor(token: Token, modifier: string, operator: string);
operate(date: Date): Date;
toString(): string;
}
export declare namespace AmountModifiers {
const valuesString: string;
function checkModifier(modifier: string): boolean;
}
export declare namespace SnapModifiers {
const valuesString: string;
function checkModifier(modifier: string): boolean;
}
export declare function newNowExpression(): NowExpression;
148 changes: 148 additions & 0 deletions ast/ast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var dateFn = require("date-fns");
var token_1 = require("../token");
var NowExpression = /** @class */ (function () {
function NowExpression(token) {
this.token = token;
}
NowExpression.prototype.operate = function (date) {
return date;
};
NowExpression.prototype.toString = function () {
return this.token.literal;
};
return NowExpression;
}());
exports.NowExpression = NowExpression;
var ModifierExpression = /** @class */ (function () {
function ModifierExpression(token, amount, operator, modifier) {
if (amount === void 0) { amount = 1; }
this.token = token;
this.amount = amount;
this.operator = operator;
this.modifier = modifier;
}
ModifierExpression.prototype.operate = function (date) {
// Lazy enough for not to type nested objects
switch (this.operator) {
case token_1.TokenType.PLUS:
switch (this.modifier) {
case 's':
return dateFn.addSeconds(date, this.amount);
case 'm':
return dateFn.addMinutes(date, this.amount);
case 'h':
return dateFn.addHours(date, this.amount);
case 'd':
return dateFn.addDays(date, this.amount);
case 'w':
return dateFn.addWeeks(date, this.amount);
case 'M':
return dateFn.addMonths(date, this.amount);
}
break;
case token_1.TokenType.MINUS:
switch (this.modifier) {
case 's':
return dateFn.subSeconds(date, this.amount);
case 'm':
return dateFn.subMinutes(date, this.amount);
case 'h':
return dateFn.subHours(date, this.amount);
case 'd':
return dateFn.subDays(date, this.amount);
case 'w':
return dateFn.subWeeks(date, this.amount);
case 'M':
return dateFn.subMonths(date, this.amount);
}
break;
}
return date;
};
ModifierExpression.prototype.toString = function () {
return "" + this.operator + this.amount + this.modifier;
};
return ModifierExpression;
}());
exports.ModifierExpression = ModifierExpression;
var SnapExpression = /** @class */ (function () {
function SnapExpression(token, modifier, operator) {
this.token = token;
this.modifier = modifier;
this.operator = operator;
}
SnapExpression.prototype.operate = function (date) {
// Lazy enough for not to type nested objects
switch (this.operator) {
case token_1.TokenType.SLASH:
switch (this.modifier) {
case 's':
return dateFn.startOfSecond(date);
case 'm':
return dateFn.startOfMinute(date);
case 'h':
return dateFn.startOfHour(date);
case 'd':
return dateFn.startOfDay(date);
case 'w':
case 'bw':
return dateFn.startOfWeek(date);
case 'M':
return dateFn.startOfMonth(date);
}
break;
case token_1.TokenType.AT:
switch (this.modifier) {
case 's':
return dateFn.endOfSecond(date);
case 'm':
return dateFn.endOfMinute(date);
case 'h':
return dateFn.endOfHour(date);
case 'd':
return dateFn.endOfDay(date);
case 'w':
return dateFn.endOfWeek(date);
case 'M':
return dateFn.endOfMonth(date);
case 'bw': {
if (dateFn.isThisWeek(date) && !dateFn.isWeekend(date)) {
return date;
}
return dateFn.endOfDay(dateFn.addDays(dateFn.startOfWeek(date), 5));
}
}
break;
}
return date;
};
SnapExpression.prototype.toString = function () {
return "" + this.operator + this.modifier;
};
return SnapExpression;
}());
exports.SnapExpression = SnapExpression;
var AmountModifiers;
(function (AmountModifiers) {
var values = ['s', 'm', 'h', 'd', 'w', 'M'];
AmountModifiers.valuesString = "(" + values.map(function (v) { return "\"" + v + "\""; }).join(',') + ")";
function checkModifier(modifier) {
return values.includes(modifier);
}
AmountModifiers.checkModifier = checkModifier;
})(AmountModifiers = exports.AmountModifiers || (exports.AmountModifiers = {}));
var SnapModifiers;
(function (SnapModifiers) {
var values = ['s', 'm', 'h', 'd', 'w', 'bw', 'M'];
SnapModifiers.valuesString = "(" + values.map(function (v) { return "\"" + v + "\""; }).join(',') + ")";
function checkModifier(modifier) {
return values.includes(modifier);
}
SnapModifiers.checkModifier = checkModifier;
})(SnapModifiers = exports.SnapModifiers || (exports.SnapModifiers = {}));
function newNowExpression() {
return new NowExpression(new token_1.Token(token_1.TokenType.NOW, 'now'));
}
exports.newNowExpression = newNowExpression;
1 change: 1 addition & 0 deletions ast/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ast';
6 changes: 6 additions & 0 deletions ast/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./ast"));
2 changes: 2 additions & 0 deletions exceptions/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export declare class InvalidTokenError extends Error {
}
23 changes: 23 additions & 0 deletions exceptions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var InvalidTokenError = /** @class */ (function (_super) {
__extends(InvalidTokenError, _super);
function InvalidTokenError() {
return _super !== null && _super.apply(this, arguments) || this;
}
return InvalidTokenError;
}(Error));
exports.InvalidTokenError = InvalidTokenError;
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Token } from './models';
export { tokenToDate } from './utils';
6 changes: 6 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var models_1 = require("./models");
exports.Token = models_1.Token;
var utils_1 = require("./utils");
exports.tokenToDate = utils_1.tokenToDate;
1 change: 1 addition & 0 deletions lexer/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lexer';
6 changes: 6 additions & 0 deletions lexer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./lexer"));
13 changes: 13 additions & 0 deletions lexer/lexer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Token } from '../token';
export declare class Lexer {
private position;
private readPosition;
private readonly input;
private currentChar;
constructor(input?: string);
nextToken(): Token;
private readChar;
private peekChar;
private readNumber;
private readWord;
}
81 changes: 81 additions & 0 deletions lexer/lexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var token_1 = require("../token");
function isDigit(payload) {
return /^\d+$/.test(payload);
}
function isLetter(payload) {
return /^\w+$/.test(payload);
}
var Lexer = /** @class */ (function () {
function Lexer(input) {
if (input === void 0) { input = ''; }
this.position = this.readPosition = 0;
this.input = input.trim();
this.currentChar = '';
this.readChar();
}
Lexer.prototype.nextToken = function () {
var token;
if (this.currentChar === '') {
return new token_1.Token(token_1.TokenType.END, this.currentChar);
}
else if (this.currentChar === '+') {
token = new token_1.Token(token_1.TokenType.PLUS, this.currentChar);
}
else if (this.currentChar === '-') {
token = new token_1.Token(token_1.TokenType.MINUS, this.currentChar);
}
else if (this.currentChar === '/') {
token = new token_1.Token(token_1.TokenType.SLASH, this.currentChar);
}
else if (this.currentChar === '@') {
token = new token_1.Token(token_1.TokenType.AT, this.currentChar);
}
else if (isDigit(this.currentChar)) {
return new token_1.Token(token_1.TokenType.NUMBER, this.readNumber());
}
else if (isLetter(this.currentChar)) {
var literal = this.readWord();
return new token_1.Token(token_1.lookupIdentifier(literal), literal);
}
else {
token = new token_1.Token(token_1.TokenType.ILLEGAL, this.currentChar);
}
this.readChar();
return token;
};
Lexer.prototype.readChar = function () {
if (this.position >= this.input.length) {
this.readPosition = 0;
this.currentChar = '';
}
else {
this.currentChar = this.input[this.readPosition];
this.position = this.readPosition;
}
this.readPosition++;
};
Lexer.prototype.peekChar = function () {
if (this.position >= this.input.length) {
return '';
}
return this.input[this.readPosition];
};
Lexer.prototype.readNumber = function () {
var pos = this.position;
while (isDigit(this.currentChar)) {
this.readChar();
}
return this.input.substring(pos, this.position);
};
Lexer.prototype.readWord = function () {
var pos = this.position;
while (isLetter(this.currentChar)) {
this.readChar();
}
return this.input.substring(pos, this.position);
};
return Lexer;
}());
exports.Lexer = Lexer;
1 change: 1 addition & 0 deletions lexer/lexer.spec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
56 changes: 56 additions & 0 deletions lexer/lexer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var lexer_1 = require("./lexer");
var token_1 = require("../token");
describe('Lexer', function () {
it('Lexer.nextToken tokenize ok', function () {
var input = 'now-1h/h@M+2w/bw-3s-49d/m';
var lexer = new lexer_1.Lexer(input);
var expected = [
[token_1.TokenType.NOW, 'now'],
[token_1.TokenType.MINUS, '-'],
[token_1.TokenType.NUMBER, '1'],
[token_1.TokenType.MODIFIER, 'h'],
[token_1.TokenType.SLASH, '/'],
[token_1.TokenType.MODIFIER, 'h'],
[token_1.TokenType.AT, '@'],
[token_1.TokenType.MODIFIER, 'M'],
[token_1.TokenType.PLUS, '+'],
[token_1.TokenType.NUMBER, '2'],
[token_1.TokenType.MODIFIER, 'w'],
[token_1.TokenType.SLASH, '/'],
[token_1.TokenType.MODIFIER, 'bw'],
[token_1.TokenType.MINUS, '-'],
[token_1.TokenType.NUMBER, '3'],
[token_1.TokenType.MODIFIER, 's'],
[token_1.TokenType.MINUS, '-'],
[token_1.TokenType.NUMBER, '49'],
[token_1.TokenType.MODIFIER, 'd'],
[token_1.TokenType.SLASH, '/'],
[token_1.TokenType.MODIFIER, 'm'],
[token_1.TokenType.END, ''],
];
for (var _i = 0, expected_1 = expected; _i < expected_1.length; _i++) {
var expectedNode = expected_1[_i];
var actual = lexer.nextToken();
expect(actual.type).toBe(expectedNode[0]);
expect(actual.literal).toBe(expectedNode[1]);
}
});
it('Lexer.nextToken tokenize illegal', function () {
var input = 'now*2h';
var lexer = new lexer_1.Lexer(input);
var expected = [
[token_1.TokenType.NOW, 'now'],
[token_1.TokenType.ILLEGAL, '*'],
[token_1.TokenType.NUMBER, '2'],
[token_1.TokenType.MODIFIER, 'h'],
];
for (var _i = 0, expected_2 = expected; _i < expected_2.length; _i++) {
var expectedNode = expected_2[_i];
var actual = lexer.nextToken();
expect(actual.type).toBe(expectedNode[0]);
expect(actual.literal).toBe(expectedNode[1]);
}
});
});
1 change: 1 addition & 0 deletions models/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './models';
Loading

0 comments on commit 0b9adf0

Please sign in to comment.