"smart" date values can now freely contain whitespaces

This commit is contained in:
zadam 2020-08-23 21:53:50 +02:00
parent a6c79c934c
commit 46e373e822
7 changed files with 79 additions and 63 deletions

View File

@ -33,9 +33,6 @@ find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app
rm -r $DIR/node_modules/sqlite3/build
rm -r $DIR/node_modules/sqlite3/deps
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs

View File

@ -65,6 +65,11 @@ describe("Lexer fulltext", () => {
.toEqual(["what's", "u=p", "<b(r*t)h>"]);
});
it("operator characters in expressions are separate tokens", () => {
expect(lex("# abc+=-def**-+d").expressionTokens.map(t => t.token))
.toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
});
it("escaping special characters", () => {
expect(lex("hello \\#\\~\\'").fulltextTokens.map(t => t.token))
.toEqual(["hello", "#~'"]);

View File

@ -219,7 +219,7 @@ describe("Invalid expressions", () => {
parsingContext
});
expect(parsingContext.error).toEqual(`Error near token "note" in "#first = note.relations.s...", it's possible to compare with constant only.`);
expect(parsingContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`);
const rootExp = parse({
fulltextTokens: [],

View File

@ -121,11 +121,11 @@ describe("Search", () => {
const parsingContext = new ParsingContext();
let searchResults = searchService.findNotesWithQuery('#established <= 1955-01-01', parsingContext);
let searchResults = searchService.findNotesWithQuery('#established <= "1955-01-01"', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Hungary")).toBeTruthy();
searchResults = searchService.findNotesWithQuery('#established > 1955-01-01', parsingContext);
searchResults = searchService.findNotesWithQuery('#established > "1955-01-01"', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
@ -154,20 +154,30 @@ describe("Search", () => {
}
test("#year = YEAR", 1);
test("#year = 'YEAR'", 0);
test("#year >= YEAR", 1);
test("#year <= YEAR", 1);
test("#year < YEAR+1", 1);
test("#year < YEAR + 1", 1);
test("#year < year + 1", 1);
test("#year > YEAR+1", 0);
test("#month = MONTH", 1);
test("#month = month", 1);
test("#month = 'MONTH'", 0);
test("#date = TODAY", 1);
test("#date = today", 1);
test("#date = 'today'", 0);
test("#date > TODAY", 0);
test("#date > TODAY-1", 1);
test("#date > TODAY - 1", 1);
test("#date < TODAY+1", 1);
test("#date < TODAY + 1", 1);
test("#date < 'TODAY + 1'", 1);
test("#dateTime <= NOW+10", 1);
test("#dateTime <= NOW + 10", 1);
test("#dateTime < NOW-10", 0);
test("#dateTime >= NOW-10", 1);
test("#dateTime < NOW-10", 0);

View File

@ -1,5 +1,3 @@
const dayjs = require("dayjs");
const stringComparators = {
"=": comparedValue => (val => val === comparedValue),
"!=": comparedValue => (val => val !== comparedValue),
@ -19,52 +17,9 @@ const numericComparators = {
"<=": comparedValue => (val => parseFloat(val) <= comparedValue)
};
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {
const match = smartValueRegex.exec(v);
if (match === null) {
return v;
}
const keyword = match[1].toUpperCase();
const num = match[2] ? parseInt(match[2].replace(/ /g, "")) : 0; // can contain spaces between sign and digits
let format, date;
if (keyword === 'NOW') {
date = dayjs().add(num, 'second');
format = "YYYY-MM-DD HH:mm:ss";
}
else if (keyword === 'TODAY') {
date = dayjs().add(num, 'day');
format = "YYYY-MM-DD";
}
else if (keyword === 'WEEK') {
// FIXME: this will always use sunday as start of the week
date = dayjs().startOf('week').add(7 * num, 'day');
format = "YYYY-MM-DD";
}
else if (keyword === 'MONTH') {
date = dayjs().add(num, 'month');
format = "YYYY-MM";
}
else if (keyword === 'YEAR') {
date = dayjs().add(num, 'year');
format = "YYYY";
}
else {
throw new Error("Unrecognized keyword: " + keyword);
}
return date.format(format);
}
function buildComparator(operator, comparedValue) {
comparedValue = comparedValue.toLowerCase();
comparedValue = calculateSmartValue(comparedValue);
if (operator in numericComparators && !isNaN(comparedValue)) {
return numericComparators[operator](parseFloat(comparedValue));
}

View File

@ -9,7 +9,7 @@ function lex(str) {
let currentWord = '';
function isSymbolAnOperator(chr) {
return ['=', '*', '>', '<', '!'].includes(chr);
return ['=', '*', '>', '<', '!', "-", "+"].includes(chr);
}
function isPreviousSymbolAnOperator() {

View File

@ -1,5 +1,6 @@
"use strict";
const dayjs = require("dayjs");
const AndExp = require('../expressions/and.js');
const OrExp = require('../expressions/or.js');
const NotExp = require('../expressions/not.js');
@ -14,7 +15,7 @@ const NoteCacheFulltextExp = require('../expressions/note_cache_flat_text.js');
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js');
const comparatorBuilder = require('./build_comparator.js');
const buildComparator = require('./build_comparator.js');
const ValueExtractor = require('../value_extractor.js');
function getFulltext(tokens, parsingContext) {
@ -41,7 +42,7 @@ function getFulltext(tokens, parsingContext) {
return new AndExp([
textSearchExpression,
new PropertyComparisonExp("isarchived", comparatorBuilder("=", "false"))
new PropertyComparisonExp("isarchived", buildComparator("=", "false"))
]);
}
@ -69,6 +70,55 @@ function getExpression(tokens, parsingContext, level = 0) {
+ (endIndex !== parsingContext.originalQuery.length ? "..." : "") + '"';
}
function resolveConstantOperand() {
const operand = tokens[i];
if (!operand.inQuotes
&& (operand.token.startsWith('#') || operand.token.startsWith('~') || operand.token === 'note')) {
parsingContext.addError(`Error near token "${operand.token}" in ${context(i)}, it's possible to compare with constant only.`);
return null;
}
if (operand.inQuotes || !["now", "today", "month", "year"].includes(operand.token)) {
return operand.token;
}
let delta = 0;
if (i + 2 < tokens.length) {
if (tokens[i + 1].token === '+') {
delta += parseInt(tokens[i + 2].token);
}
else if (tokens[i + 1].token === '-') {
delta -= parseInt(tokens[i + 2].token);
}
}
let format, date;
if (operand.token === 'now') {
date = dayjs().add(delta, 'second');
format = "YYYY-MM-DD HH:mm:ss";
}
else if (operand.token === 'today') {
date = dayjs().add(delta, 'day');
format = "YYYY-MM-DD";
}
else if (operand.token === 'month') {
date = dayjs().add(delta, 'month');
format = "YYYY-MM";
}
else if (operand.token === 'year') {
date = dayjs().add(delta, 'year');
format = "YYYY";
}
else {
throw new Error("Unrecognized keyword: " + operand.token);
}
return date.format(format);
}
function parseNoteProperty() {
if (tokens[i].token !== '.') {
parsingContext.addError('Expected "." to separate field path');
@ -150,7 +200,7 @@ function getExpression(tokens, parsingContext, level = 0) {
const propertyName = tokens[i].token;
const operator = tokens[i + 1].token;
const comparedValue = tokens[i + 2].token;
const comparator = comparatorBuilder(operator, comparedValue);
const comparator = buildComparator(operator, comparedValue);
if (!comparator) {
parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`);
@ -186,11 +236,12 @@ function getExpression(tokens, parsingContext, level = 0) {
if (i < tokens.length - 2 && isOperator(tokens[i + 1].token)) {
let operator = tokens[i + 1].token;
const comparedValue = tokens[i + 2].token;
if (!tokens[i + 2].inQuotes
&& (comparedValue.startsWith('#') || comparedValue.startsWith('~') || comparedValue === 'note')) {
parsingContext.addError(`Error near token "${comparedValue}" in ${context(i)}, it's possible to compare with constant only.`);
i += 2;
const comparedValue = resolveConstantOperand();
if (comparedValue === null) {
return;
}
@ -200,13 +251,11 @@ function getExpression(tokens, parsingContext, level = 0) {
operator = '*=*';
}
const comparator = comparatorBuilder(operator, comparedValue);
const comparator = buildComparator(operator, comparedValue);
if (!comparator) {
parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`);
parsingContext.addError(`Can't find operator '${operator}' in ${context(i - 1)}`);
} else {
i += 2;
return new LabelComparisonExp('label', labelName, comparator);
}
} else {