@silane/datetime
Version:
Date and time library similar to Python's "datetime" package.
404 lines (382 loc) • 10.8 kB
JavaScript
import {
neg, add, sub, cmp, TimeDelta, Date, Time, DateTime
} from "./datetime.js";
import { DateTimeError, NotImplementedDateTimeError } from './errors.js';
/**
* Base exception of other exceptions raised in dtexpr.
*/
export class DtexprDateTimeError extends DateTimeError {
/**
* @param {*[]} expression The expression caused this error.
* @param {[number, number]} pos Position of the error in the expression.
* @param {string} message Error message.
*/
constructor(expression, pos, message) {
super(message);
this.expression = expression;
this.pos = pos;
}
toString() {
const [pos1, pos2] = this.pos;
const expressionStr = this.expression.map(
x => typeof x === 'string' ? x : '${...}').join('');
const pos = this.expression.slice(0, pos1).map(
x => typeof x === 'string' ? x.length : 6
).reduce((acc, cur) => acc + cur, 0) + pos2;
return `[${this.constructor.name}] ${this.message}
${expressionStr}
${' '.repeat(pos)}^`;
}
}
/**
* Raised when there is a syntax error in dtexpr.
*/
export class SyntaxDtexprDateTimeError extends DtexprDateTimeError {
}
/**
* Raised when there occur an error in execution phase in dtexpr, such as
* inappropriate type passed to an operator.
*/
export class ExecutionDtexprDateTimeError extends DtexprDateTimeError {
/**
* @param {*[]} expression The expression caused this error.
* @param {[number, number]} pos Position of the error in the expression.
* @param {?Error} originalError The original error object.
* @param {string} message Error message.
*/
constructor(expression, pos, originalError, message) {
super(expression, pos, message);
this.originalError = originalError;
}
toString() {
if(this.originalError) {
return `${super.toString()}
Original Error: ${this.originalError}`;
} else {
return super.toString();
}
}
}
class Variable {
constructor(name) {
this.name = name;
}
}
class Node {
constructor(pos) {
this.pos = pos;
}
execute(context) {
throw new NotImplementedDateTimeError();
}
}
class VariableNode extends Node {
constructor(variableName, pos) {
super(pos);
this.variableName = variableName;
}
execute(context) {
if(this.variableName in context.variables) {
return context.variables[this.variableName];
} else {
throw new ExecutionDtexprDateTimeError(
[], this.pos, null,
`Varibale "${this.variableName}" is not defined in the execution context`,
);
}
}
}
class NegNode extends Node {
constructor(node, pos) {
super(pos);
this.node = node;
}
execute(context) {
const value = this.node.execute(context);
try {
return neg(value);
} catch(e) {
if(e instanceof DateTimeError) {
throw new ExecutionDtexprDateTimeError(
[], this.pos, e, 'Execution error in negation operator.'
);
}
throw e;
}
}
}
class BinaryNode extends Node {
constructor(lhs, rhs, pos) {
super(pos);
this.lhs = lhs;
this.rhs = rhs;
}
}
class CommonBinaryNode extends BinaryNode {
/** @type {string} */
get operatorName() {
throw new NotImplementedDateTimeError();
}
operate(leftValue, rightValue) {
throw new NotImplementedDateTimeError();
}
execute(context) {
const lVal = this.lhs.execute(context);
const rVal = this.rhs.execute(context);
try {
return this.operate(lVal, rVal);
} catch(e) {
if(e instanceof DateTimeError) {
throw new ExecutionDtexprDateTimeError(
[], this.pos, e,
`Execution error in ${this.operatorName} operator.`,
);
}
throw e;
}
}
}
class LesserNode extends CommonBinaryNode {
get operatorName() {
return 'lesser-than';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) < 0;
}
}
class LesserEqualNode extends CommonBinaryNode {
get operatorName() {
return 'lesser-or-equal';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) <= 0;
}
}
class EqualNode extends CommonBinaryNode {
get operatorName() {
return 'equal';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) == 0;
}
}
class NotEqualNode extends CommonBinaryNode {
get operatorName() {
return 'not-equal';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) != 0;
}
}
class GreaterNode extends CommonBinaryNode {
get operatorName() {
return 'greater-than';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) > 0;
}
}
class GreaterEqualNode extends CommonBinaryNode {
get operatorName() {
return 'greater-or-equal';
}
operate(leftValue, rightValue) {
return cmp(leftValue, rightValue) >= 0;
}
}
class AddNode extends CommonBinaryNode {
get operatorName() {
return 'addition';
}
operate(leftValue, rightValue) {
return add(leftValue, rightValue);
}
}
class SubNode extends CommonBinaryNode {
get operatorName() {
return 'subtraction';
}
operate(leftValue, rightValue) {
return sub(leftValue, rightValue);
}
}
class ParsingStr {
/**
* @param {(string|number|!TimeDelta|!Date|!Time|!DateTime)[]} s
*/
constructor(s) {
this.s = s;
this.pos1 = 0;
this.pos2 = 0;
}
consumeIf(startstr) {
const item = this.s[this.pos1];
if(typeof item === 'string') {
if(item.slice(this.pos2).startsWith(startstr)) {
this.pos2 += startstr.length;
if(this.pos2 >= item.length) {
++this.pos1;
this.pos2 = 0;
}
return startstr;
} else {
return '';
}
} else {
return '';
}
}
consumeWhile(chars) {
const item = this.s[this.pos1];
if(typeof item === 'string') {
let ret = '';
for(; this.pos2 < item.length; ++this.pos2) {
if(chars.includes(this.s[this.pos1][this.pos2]))
ret += this.s[this.pos1][this.pos2];
else
break;
}
if(this.pos2 >= item.length) {
++this.pos1;
this.pos2 = 0;
}
return ret;
} else {
return '';
}
}
consumeIfVariable() {
if(this.s[this.pos1] instanceof Variable) {
return this.s[this.pos1++];
} else {
return null;
}
}
}
function whitespace(s) {
s.consumeWhile(' \n');
}
function expr(s) {
whitespace(s);
const lhs = poly(s);
const pos = [s.pos1, s.pos2];
if(s.consumeIf('<=')) {
whitespace(s);
const rhs = expr(s);
return new LesserEqualNode(lhs, rhs, pos);
} else if(s.consumeIf('<')) {
whitespace(s);
const rhs = expr(s);
return new LesserNode(lhs, rhs, pos);
} else if(s.consumeIf('==')) {
whitespace(s);
const rhs = expr(s);
return new EqualNode(lhs, rhs, pos);
} else if(s.consumeIf('!=')) {
whitespace(s);
const rhs = expr(s);
return new NotEqualNode(lhs, rhs, pos);
} else if(s.consumeIf('>=')) {
whitespace(s);
const rhs = expr(s);
return new GreaterEqualNode(lhs, rhs, pos);
} else if(s.consumeIf('>')) {
whitespace(s);
const rhs = expr(s);
return new GreaterNode(lhs, rhs, pos);
} else {
return lhs;
}
}
function poly(s) {
whitespace(s);
const lhs = term(s);
whitespace(s);
const pos = [s.pos1, s.pos2];
if(s.consumeIf('+')) {
whitespace(s);
const rhs = poly(s);
return new AddNode(lhs, rhs, pos);
} else if(s.consumeIf('-')) {
whitespace(s);
const rhs = poly(s);
return new SubNode(lhs, rhs, pos);
} else {
return lhs;
}
}
function term(s) {
return negterm(s);
}
function negterm(s) {
whitespace(s);
const pos = [s.pos1, s.pos2];
if(s.consumeIf('-')) {
whitespace(s);
const node = realexpr(s);
return new NegNode(node, pos);
} else {
return realexpr(s);
}
}
function realexpr(s) {
whitespace(s);
/** @type {[number, number]} */
const pos = [s.pos1, s.pos2];
if(s.consumeIf('(')) {
whitespace(s);
const lhs = poly(s);
if(!s.consumeIf(')')) {
throw new SyntaxDtexprDateTimeError(
s.s, pos, 'Expected ")".');
}
return lhs;
}
const variable = s.consumeIfVariable();
if(variable != null) {
return new VariableNode(variable.name, pos);
}
throw new SyntaxDtexprDateTimeError(s.s, pos, 'Unexpected token.');
}
function isExpressionTokenListSame(a, b) {
if(a.length !== b.length) return false;
return a.map((x, i) => [x, b[i]]).every(([aToken, bToken]) => {
if(aToken === bToken) return true;
return (
aToken instanceof Variable && bToken instanceof Variable
&& aToken.name === bToken.name
);
});
}
const expressionCache = [];
/**
* Tagged template function to perform operations on datetime objects.
* @param {string[]} strings Strings to be passed by tagged template.
* @param {...*} values Values to be passed by tagged template.
*/
export function dtexpr(strings, ...values) {
const variables = values.map((x, i) => [new Variable(`var_${i}`), x]);
let list = [strings[0]];
variables.forEach((variable, i) => {
list.push(variable[0]);
list.push(strings[i + 1]);
});
list = list.filter(x => !(typeof x === 'string' && !x));
let expression = expressionCache.find(
x => isExpressionTokenListSame(x[0], list)
)?.[1] ?? null;
if(!expression) {
expression = expr(new ParsingStr(list));
expressionCache.push([list, expression]);
}
try {
return expression.execute({ variables: Object.fromEntries(
variables.map(x => [x[0].name, x[1]])
)});
} catch (e) {
if(e instanceof ExecutionDtexprDateTimeError) {
e.expression = list;
}
throw e;
}
}