From 5405a6f9d3f5211a8b4f5e6ccae362c87c57f83e Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 4 Apr 2021 11:40:43 +0200 Subject: [PATCH] improving date autocomplete and parsing for search #58 --- src/common/SearchQueryParser.ts | 62 +++++++++++++------ .../ui/gallery/search/autocomplete.service.ts | 26 +++++--- test/common/unit/SearchQueryParser.ts | 16 ++++- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/common/SearchQueryParser.ts b/src/common/SearchQueryParser.ts index 20482dfc..9cc801bd 100644 --- a/src/common/SearchQueryParser.ts +++ b/src/common/SearchQueryParser.ts @@ -48,6 +48,41 @@ export class SearchQueryParser { constructor(private keywords: QueryKeywords) { } + public static stringifyText(text: string, matchType = TextSearchQueryMatchTypes.like): string { + if (matchType === TextSearchQueryMatchTypes.exact_match) { + return '"' + text + '"'; + } + if (text.indexOf(' ') !== -1) { + return '(' + text + ')'; + } + return text; + } + + private static stringifyDate(time: number): string { + const date = new Date(time); + // simplify date with yeah only if its first of jan + if (date.getMonth() === 0 && date.getDate() === 1) { + return date.getFullYear().toString(); + } + return this.stringifyText(date.toLocaleDateString()); + } + + private static parseDate(text: string): number { + if (text.charAt(0) === '"' || text.charAt(0) === '(') { + text = text.substring(1); + } + if (text.charAt(text.length - 1) === '"' || text.charAt(text.length - 1) === ')') { + text = text.substring(0, text.length - 1); + } + // it is the year only + if (text.length === 4) { + const d = new Date(2000, 0, 1); + d.setFullYear(parseInt(text, 10)); + return d.getTime(); + } + return Date.parse(text); + } + public parse(str: string, implicitOR = true): SearchQueryDTO { str = str.replace(/\s\s+/g, ' ') // remove double spaces .replace(/:\s+/g, ':').replace(/\)(?=\S)/g, ') ').trim(); @@ -136,13 +171,13 @@ export class SearchQueryParser { if (str.startsWith(this.keywords.from + ':')) { return { type: SearchQueryTypes.from_date, - value: Date.parse(str.slice((this.keywords.from + ':').length + 1, str.length - 1)) + value: SearchQueryParser.parseDate(str.substring((this.keywords.from + ':').length)) }; } if (str.startsWith(this.keywords.to + ':')) { return { type: SearchQueryTypes.to_date, - value: Date.parse(str.slice((this.keywords.to + ':').length + 1, str.length - 1)) + value: SearchQueryParser.parseDate(str.substring((this.keywords.to + ':').length)) }; } @@ -240,12 +275,14 @@ export class SearchQueryParser { if (!(query).value) { return ''; } - return this.keywords.from + ':(' + new Date((query).value).toLocaleDateString() + ')'.trim(); + return this.keywords.from + ':' + + SearchQueryParser.stringifyDate((query).value); case SearchQueryTypes.to_date: if (!(query).value) { return ''; } - return this.keywords.to + ':(' + new Date((query).value).toLocaleDateString() + ')'.trim(); + return this.keywords.to + ':' + + SearchQueryParser.stringifyDate((query).value); case SearchQueryTypes.min_rating: return this.keywords.minRating + ':' + (isNaN((query).value) ? '' : (query).value); case SearchQueryTypes.max_rating: @@ -261,13 +298,7 @@ export class SearchQueryParser { return (query).distance + '-' + this.keywords.kmFrom + ':' + (query).from.text; case SearchQueryTypes.any_text: - if ((query).matchType === TextSearchQueryMatchTypes.exact_match) { - return '"' + (query).text + '"'; - - } else if ((query).text.indexOf(' ') !== -1) { - return '(' + (query).text + ')'; - } - return (query).text; + return SearchQueryParser.stringifyText((query).text, (query).matchType); case SearchQueryTypes.person: case SearchQueryTypes.position: @@ -278,13 +309,8 @@ export class SearchQueryParser { if (!(query).text) { return ''; } - if ((query).matchType === TextSearchQueryMatchTypes.exact_match) { - return (this.keywords)[SearchQueryTypes[query.type]] + ':"' + (query).text + '"'; - - } else if ((query).text.indexOf(' ') !== -1) { - return (this.keywords)[SearchQueryTypes[query.type]] + ':(' + (query).text + ')'; - } - return (this.keywords)[SearchQueryTypes[query.type]] + ':' + (query).text; + return (this.keywords)[SearchQueryTypes[query.type]] + ':' + + SearchQueryParser.stringifyText((query).text, (query).matchType); default: throw new Error('Unknown type: ' + query.type); diff --git a/src/frontend/app/ui/gallery/search/autocomplete.service.ts b/src/frontend/app/ui/gallery/search/autocomplete.service.ts index 7a9134b3..474942dc 100644 --- a/src/frontend/app/ui/gallery/search/autocomplete.service.ts +++ b/src/frontend/app/ui/gallery/search/autocomplete.service.ts @@ -6,6 +6,7 @@ import {SearchQueryParserService} from './search-query-parser.service'; import {BehaviorSubject} from 'rxjs'; import {SearchQueryTypes, TextSearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO'; import {QueryParams} from '../../../../../common/QueryParams'; +import {SearchQueryParser} from '../../../../../common/SearchQueryParser'; @Injectable() export class AutoCompleteService { @@ -31,6 +32,15 @@ export class AutoCompleteService { this.keywords.push(i + this._searchQueryParserService.keywords.NSomeOf); } + this.keywords.push(this._searchQueryParserService.keywords.to + ':' + + SearchQueryParser.stringifyText((new Date).getFullYear().toString())); + this.keywords.push(this._searchQueryParserService.keywords.to + ':' + + SearchQueryParser.stringifyText((new Date).toLocaleDateString())); + this.keywords.push(this._searchQueryParserService.keywords.from + ':' + + SearchQueryParser.stringifyText((new Date).getFullYear().toString())); + this.keywords.push(this._searchQueryParserService.keywords.from + ':' + + SearchQueryParser.stringifyText((new Date).toLocaleDateString())); + TextSearchQueryTypes.forEach(t => { this.textSearchKeywordsMap[(this._searchQueryParserService.keywords)[SearchQueryTypes[t]]] = t; }); @@ -69,14 +79,6 @@ export class AutoCompleteService { return items; } - private getTypeFromPrefix(text: string): SearchQueryTypes { - const tokens = text.split(':'); - if (tokens.length !== 2) { - return null; - } - return this.textSearchKeywordsMap[tokens[0]] || null; - } - public getPrefixLessSearchText(text: string): string { const tokens = text.split(':'); if (tokens.length !== 2) { @@ -89,6 +91,14 @@ export class AutoCompleteService { return tokens[1]; } + private getTypeFromPrefix(text: string): SearchQueryTypes { + const tokens = text.split(':'); + if (tokens.length !== 2) { + return null; + } + return this.textSearchKeywordsMap[tokens[0]] || null; + } + private ACItemToRenderable(item: IAutoCompleteItem): RenderableAutoCompleteItem { if (!item.type) { return {text: item.text, queryHint: item.text}; diff --git a/test/common/unit/SearchQueryParser.ts b/test/common/unit/SearchQueryParser.ts index 442a1ca7..1caccecd 100644 --- a/test/common/unit/SearchQueryParser.ts +++ b/test/common/unit/SearchQueryParser.ts @@ -9,6 +9,7 @@ import { MinResolutionSearch, OrientationSearch, ORSearchQuery, + RangeSearch, SearchQueryDTO, SearchQueryTypes, SomeOfSearchQuery, @@ -66,8 +67,21 @@ describe('SearchQueryParser', () => { }); it('Date search', () => { + check({type: SearchQueryTypes.from_date, value: (new Date(2020, 1, 10)).getTime()}); check({type: SearchQueryTypes.from_date, value: (new Date(2020, 1, 1)).getTime()}); - check({type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 2)).getTime()}); + check({type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 20)).getTime()}); + check({type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 1)).getTime()}); + + const parser = new SearchQueryParser(keywords); + // test if date gets simplified on 1st of Jan. + let query: RangeSearch = {type: SearchQueryTypes.to_date, value: (new Date(2020, 0, 1)).getTime()}; + expect(parser.parse(keywords.to + ':' + (new Date(query.value)).getFullYear())) + .to.deep.equals(query, parser.stringify(query)); + + query = {type: SearchQueryTypes.from_date, value: (new Date(2020, 0, 1)).getTime()}; + expect(parser.parse(keywords.from + ':' + (new Date(query.value)).getFullYear())) + .to.deep.equals(query, parser.stringify(query)); + }); it('Rating search', () => { check({type: SearchQueryTypes.min_rating, value: 10});