@teipublisher/pb-components 1.32.0 → 1.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,521 @@
1
+ import { get as i18n } from "./pb-i18n.js";
2
+
3
+ export class SearchResultService {
4
+ /*
5
+ * SEARCH RESULT OBJECT
6
+ * Service that loads initial data from a datasource,
7
+ * can be a database or an API, and converts it in
8
+ * a format that can be used by the pb-timeline component.
9
+ *
10
+ * public methods:
11
+ * getMinDateStr()
12
+ * getMaxDateStr()
13
+ * getMinDate()
14
+ * getMaxDate()
15
+ * export()
16
+ * getIntervalSizes()
17
+ */
18
+
19
+ /*
20
+ * CONSRTUCTOR INPUTS EXPLAINED
21
+ * jsonData: data to load, object with
22
+ * keys => valid datestrings formatted YYYY-MM-DD
23
+ * values => number of results for this day
24
+ * maxInterval: max amount of bins allowed
25
+ * scopes: array of all 6 possible values for scope
26
+ */
27
+ constructor(jsonData = {}, maxInterval = 60, scopes = ["D", "W", "M", "Y", "5Y", "10Y"]) {
28
+ this.data = { invalid: {}, valid: {} };
29
+ this.maxInterval = maxInterval;
30
+ this.scopes = scopes;
31
+ this._validateJsonData(jsonData);
32
+ }
33
+
34
+ /*
35
+ * based on the loaded jsonData, compute
36
+ * - min date as dateStr or utc-date-object
37
+ * - max date as dateStr or utc-date-object
38
+ */
39
+ getMinDateStr() {
40
+ return Object.keys(this.data.valid).sort()[0];
41
+ }
42
+ getMaxDateStr() {
43
+ let days = Object.keys(this.data.valid);
44
+ return days.sort()[days.length - 1];
45
+ }
46
+ getMinDate() {
47
+ return this._dateStrToUTCDate(this.getMinDateStr());
48
+ }
49
+ getMaxDate() {
50
+ return this._dateStrToUTCDate(this.getMaxDateStr());
51
+ }
52
+
53
+ getEndOfRangeDate(scope, date) {
54
+ return this._UTCDateToDateStr(this._increaseDateBy(scope, date));
55
+ }
56
+
57
+ /*
58
+ * exports data for each scope
59
+ * when no argument is provided, the optimal scope based
60
+ * on the maxInterval (default 60) will be assigned
61
+ */
62
+ export(scope) {
63
+ // auto assign scope when no argument provided
64
+ scope = scope || this._autoAssignScope();
65
+ // validate scope
66
+ if (!this.scopes.includes(scope)) {
67
+ throw new Error(`invalid scope provided, expected: ["10Y", "5Y", "Y", "M", "W", "D"]. Got: "${scope}"`);
68
+ }
69
+ // initialize object to export
70
+ const exportData = {
71
+ data: [],
72
+ scope: scope,
73
+ binTitleRotated: this._binTitleRotatedLookup(scope)
74
+ }
75
+ if (Object.keys(this.data.valid).length === 0) {
76
+ return exportData;
77
+ }
78
+ // get start and end date
79
+ const startCategory = this._classify(this.getMinDateStr(), scope);
80
+ const startDateStr = this._getFirstDay(startCategory);
81
+ let currentDate = this._dateStrToUTCDate(startDateStr);
82
+ const endDate = this.getMaxDate();
83
+ // iterate until end of intervall reached, add binObject for each step
84
+ while (currentDate <= endDate) {
85
+ const currentDateStr = this._UTCDateToDateStr(currentDate);
86
+ const currentCategory = this._classify(currentDateStr, scope);
87
+ exportData.data.push(this._buildBinObject(currentDateStr, currentCategory, scope));
88
+ currentDate = this._increaseDateBy(scope, currentDate);
89
+ }
90
+ // count all values
91
+ Object.keys(this.data.valid).sort().forEach(dateStr => {
92
+ const currentCategory = this._classify(dateStr, scope);
93
+ const targetBinObject = exportData.data.find(it => it.category === currentCategory);
94
+ try {
95
+ const value = this.data.valid[dateStr];
96
+ if (typeof value === 'object') {
97
+ targetBinObject.value += value.count || 0;
98
+ if (value.info) {
99
+ targetBinObject.info = targetBinObject.info.concat(value.info);
100
+ }
101
+ } else {
102
+ targetBinObject.value += this.data.valid[dateStr] || 0;
103
+ }
104
+ } catch(e) {
105
+ console.log(e);
106
+ console.log("currentCategory");
107
+ console.log(currentCategory);
108
+ }
109
+ });
110
+ if (this.data.invalid) {
111
+ let invalid = 0;
112
+ let info = [];
113
+ Object.values(this.data.invalid).forEach((value) => {
114
+ if (typeof value === 'object') {
115
+ invalid += value.count || 0;
116
+ info = info.concat(value.info);
117
+ } else {
118
+ invalid += value;
119
+ }
120
+ });
121
+ if (invalid > 0) {
122
+ exportData.data.push({
123
+ tooltip: i18n('timeline.unknown'),
124
+ title: i18n('timeline.unknown'),
125
+ // binTitle: i18n('timeline.unknown'),
126
+ category: '?',
127
+ separator: true,
128
+ value: invalid,
129
+ info
130
+ });
131
+ }
132
+ }
133
+ return exportData;
134
+ }
135
+
136
+ /*
137
+ * returns optimal scope based on the maxInterval
138
+ * by computing the scope that meets the criteria
139
+ * nbr of bins <= maxInterval
140
+ */
141
+ _autoAssignScope() {
142
+ for (let i = 0; i < this.scopes.length; i++) {
143
+ if (this._computeIntervalSize(this.scopes[i]) <= this.maxInterval) {
144
+ return this.scopes[i];
145
+ }
146
+ }
147
+ throw new Error(`Interval too big! Computed: ${this._computeIntervalSize(this.scopes[i])}. Allowed: ${this.maxInterval}. Try to increase maxInterval.`);
148
+ }
149
+
150
+ /*
151
+ * splits input data in 2 sections
152
+ * => valid data
153
+ * => invalid (if not a vaid date, for example 2012-00-00 is invalid)
154
+ */
155
+ _validateJsonData(jsonData) {
156
+ Object.keys(jsonData).sort().forEach(key => {
157
+ if (this._isValidDateStr(key)) {
158
+ this.data.valid[key] = jsonData[key];
159
+ } else {
160
+ this.data.invalid[key] = jsonData[key];
161
+ }
162
+ });
163
+ }
164
+
165
+ /*
166
+ * lookup table which bin titles should be rotated
167
+ */
168
+ _binTitleRotatedLookup(scope) {
169
+ const lookup = {
170
+ "10Y": true,
171
+ "5Y": true,
172
+ "Y": true,
173
+ "M": false, // only exception not to rotate in monthly scope
174
+ "W": true,
175
+ "D": true,
176
+ }
177
+ return lookup[scope];
178
+ }
179
+
180
+ /*
181
+ * Helper method that builds a binObject that
182
+ * can be read by the pb-timeline component
183
+ */
184
+ _buildBinObject(dateStr, category, scope) {
185
+ const split = dateStr.split("-");
186
+ const yearStr = split[0];
187
+ const monthStr = split[1];
188
+ const dayStr = split[2];
189
+ // for all scopes this remains the same
190
+ const binObject = {
191
+ dateStr: dateStr,
192
+ category: category,
193
+ value: 0,
194
+ info: []
195
+ }
196
+ // scope specific bin data
197
+ if (scope === "10Y") {
198
+ binObject.tooltip = `${category} - ${Number(category) + 9}`; // 1900 - 1999
199
+ binObject.selectionStart = `${category}`;
200
+ binObject.selectionEnd = `${Number(category) + 9}`;
201
+ // seperator every 100 years (10 bins)
202
+ if (Number(category) % 100 === 0) {
203
+ binObject.title = `${category} - ${Number(category) + 99}`;
204
+ binObject.binTitle = category;
205
+ binObject.seperator = true;
206
+ };
207
+ } else if (scope === "5Y") {
208
+ binObject.tooltip = `${category} - ${Number(category) + 4}`; // 1995 - 1999
209
+ binObject.selectionStart = `${category}`;
210
+ binObject.selectionEnd = `${Number(category) + 4}`;
211
+ // seperator every 50 years (10 bins)
212
+ if (Number(category) % 50 === 0) {
213
+ binObject.title = `${category} - ${Number(category) + 49}`;
214
+ binObject.binTitle = category;
215
+ binObject.seperator = true;
216
+ }
217
+ } else if (scope === "Y") {
218
+ binObject.tooltip = category;
219
+ binObject.selectionStart = category;
220
+ binObject.selectionEnd = category;
221
+ // seperator every 10 years (10 bins)
222
+ if (Number(category) % 10 === 0) {
223
+ binObject.title = `${category} - ${Number(category) + 9}`;
224
+ binObject.binTitle = `${category}`;
225
+ binObject.seperator = true;
226
+ }
227
+ } else if (scope === "M") {
228
+ const monthNum = Number(monthStr);
229
+ const month = this._monthLookup(monthNum); // Jan,Feb,Mar,...,Nov,Dez
230
+ binObject.binTitle = month[0]; // J,F,M,A,M,J,J,..N,D
231
+ binObject.tooltip = `${month} ${yearStr}`; // May 1996
232
+ binObject.selectionStart = `${month} ${yearStr}`;
233
+ binObject.selectionEnd = `${month} ${yearStr}`;
234
+ // every first of the month
235
+ if (monthNum === 1) {
236
+ binObject.title = yearStr; // YYYY
237
+ binObject.seperator = true;
238
+ }
239
+ } else if (scope === "W") {
240
+ const week = category.split("-")[1];; // => W52
241
+ binObject.tooltip = `${yearStr} ${week}`; // 1996 W52
242
+ binObject.selectionStart = `${yearStr} ${week}`; // 1996 W52
243
+ binObject.selectionEnd = `${yearStr} ${week}`; // 1996 W52
244
+ let currentDate = this._dateStrToUTCDate(dateStr);
245
+ let lastWeek = this._addDays(currentDate, -7);
246
+ // title and binTitle every first monday of the month
247
+ if (currentDate.getUTCMonth() !== lastWeek.getUTCMonth()) {
248
+ binObject.binTitle = week;
249
+ binObject.title = this._monthLookup(currentDate.getUTCMonth() + 1);
250
+ }
251
+ // seperator every start of the year
252
+ binObject.seperator = week === "W1";
253
+ } else if (scope === "D") {
254
+ binObject.tooltip = dateStr;
255
+ binObject.selectionStart = dateStr;
256
+ binObject.selectionEnd = dateStr;
257
+ // every monday
258
+ if (this._dateStrToUTCDate(dateStr).getUTCDay() === 1) {
259
+ binObject.binTitle = `${Number(dayStr)}.${Number(monthStr)}`;
260
+ binObject.title = `${this._classify(dateStr, "W").replace("-", " ")}`;
261
+ binObject.seperator = true;
262
+ }
263
+ } else {
264
+ throw new Error(`invalid scope provided, expected: ["10Y", "5Y", "Y", "M", "W", "D"]. Got: "${scope}"`);
265
+ }
266
+ return binObject;
267
+ }
268
+
269
+ /*
270
+ * ...classifies dateStr into category (based on scope)
271
+ * EXAMPLES:
272
+ * _classify("2016-01-12", "10Y") // => "2010"
273
+ * _classify("2016-01-12", "5Y") // => "2015"
274
+ * _classify("2016-01-12", "Y") // => "2016"
275
+ * _classify("2016-01-12", "M") // => "2010-01"
276
+ * _classify("2016-01-12", "W") // => "2016-W2"
277
+ * _classify("2016-01-12", "D") // => "2016-01-12"
278
+ */
279
+ _classify(dateStr, scope) { // returns category (as string)
280
+ if (!dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { // quick validate dateStr
281
+ throw new Error(`invalid dateStr format, expected "YYYY-MM-DD", got: "${dateStr}".`);
282
+ }
283
+ if (!dateStr || !scope) { // both inputs provided
284
+ throw new Error(`both inputs must be provided. Got dateStr=${dateStr}, scope=${scope}`);
285
+ }
286
+ switch (scope) {
287
+ case "10Y": case "5Y":
288
+ const intervalSize = Number(scope.replace("Y", ""));
289
+ const startYear = Math.floor(Number(dateStr.split("-")[0]) / intervalSize) * intervalSize;
290
+ return startYear.toString();
291
+ case "Y":
292
+ return dateStr.substr(0, 4);
293
+ case "M":
294
+ return dateStr.substr(0, 7);
295
+ case "W":
296
+ const UTCDate = this._dateStrToUTCDate(dateStr);
297
+ return this._UTCDateToWeekFormat(UTCDate);
298
+ case "D":
299
+ return dateStr;
300
+ }
301
+ }
302
+
303
+ /*
304
+ * ...gets first day as UTC Date, based on the category
305
+ * EXAMPLES:
306
+ * _getFirstDay("2010") // => 2010-01-01
307
+ * _getFirstDay("2010-12") // => 2010-12-01
308
+ * _getFirstDay("2010-W10") // => 2010-03-08
309
+ */
310
+ _getFirstDay(categoryStr) {
311
+ if (categoryStr.match(/^\d{4}-\d{2}-\d{2}$/)) { // YYYY-MM-DD => return same value
312
+ return categoryStr;
313
+ }
314
+ if (categoryStr.match(/^\d{4}-\d{2}$/)) { // YYYY-MM
315
+ return `${categoryStr}-01`; // add -01
316
+ }
317
+ if (categoryStr.match(/^\d{4}$/)) { // YYYY
318
+ return `${categoryStr}-01-01`; // add -01-01
319
+ }
320
+ if (categoryStr.match(/^\d{4}-W([1-9]|[1-4][0-9]|5[0-3])$/)) { // YYYY-W? // ? => [1-53]
321
+ // |YYYY-W |1-9 | 10-49 | 50-53 |
322
+ const split = categoryStr.split("-");
323
+ const year = Number(split[0]);
324
+ const weekNumber = Number(split[1].replace("W", ""));
325
+ return this._getDateStrOfISOWeek(year, weekNumber);
326
+ }
327
+ throw new Error("invalid categoryStr");
328
+ }
329
+
330
+ /*
331
+ * converts dateStr (YYYY-MM-DD) to a date object in UTC time
332
+ */
333
+ _dateStrToUTCDate(dateStr) {
334
+ if (!this._isValidDateStr(dateStr)) {
335
+ throw new Error(`invalid dateStr, expected "YYYY-MM-DD" with month[1-12] and day[1-31], got: "${dateStr}".`);
336
+ }
337
+ const split = dateStr.split("-");
338
+ const year = Number(split[0]);
339
+ const month = Number(split[1]);
340
+ const day = Number(split[2]);
341
+ return new Date(Date.UTC(year, month - 1, day));
342
+ }
343
+
344
+ /*
345
+ * converts a UTC date object to a dateStr (YYYY-MM-DD)
346
+ */
347
+ _UTCDateToDateStr(UTCDate) {
348
+ return UTCDate.toISOString().split("T")[0];
349
+ }
350
+
351
+ /*
352
+ * example:
353
+ * 1 Jan 2020 => 2020-W1
354
+ */
355
+ _UTCDateToWeekFormat(UTCDate) {
356
+ const year = this._getISOWeekYear(UTCDate);
357
+ const weekNbr = this._getISOWeek(UTCDate);
358
+ return `${year}-W${weekNbr}`;
359
+ }
360
+
361
+ /*
362
+ * returns the ISO week (_getISOWeek) or year (_getISOWeekYear)
363
+ * as number based on a UTC date.
364
+ */
365
+ _getISOWeek(UTCDate) { // https://weeknumber.net/how-to/javascript
366
+ let date = new Date(UTCDate.getTime());
367
+ date.setHours(0, 0, 0, 0);
368
+ // Thursday in current week decides the year.
369
+ date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
370
+ // January 4 is always in week 1.
371
+ let week1 = new Date(date.getFullYear(), 0, 4);
372
+ // Adjust to Thursday in week 1 and count number of weeks from date to week1.
373
+ return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
374
+ - 3 + (week1.getDay() + 6) % 7) / 7);
375
+ }
376
+ /*
377
+ * returns the ISO week year as number based on a UTC date
378
+ * this is only needed for rollovers, for example:
379
+ * => 1.jan 2011 is in W52 of year 2010.
380
+ */
381
+ _getISOWeekYear(UTCDate) { // https://weeknumber.net/how-to/javascript
382
+ var date = new Date(UTCDate.getTime());
383
+ date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
384
+ return date.getFullYear();
385
+ }
386
+
387
+ /*
388
+ * given the year and weeknumber -> return dateStr (YYYY-MM-DD)
389
+ */
390
+ _getDateStrOfISOWeek(year, weekNumber) { // https://stackoverflow.com/a/16591175/6272061
391
+ let simple = new Date(Date.UTC(year, 0, 1 + (weekNumber - 1) * 7));
392
+ let dow = simple.getUTCDay();
393
+ let ISOweekStart = simple;
394
+ if (dow <= 4)
395
+ ISOweekStart.setDate(simple.getDate() - simple.getUTCDay() + 1);
396
+ else
397
+ ISOweekStart.setDate(simple.getDate() + 8 - simple.getUTCDay());
398
+ return ISOweekStart.toISOString().split("T")[0];
399
+ }
400
+
401
+ /*
402
+ * compute the interval sizes based on the scope
403
+ * prediction only, not the actuall export of the data
404
+ */
405
+ getIntervalSizes() {
406
+ return {
407
+ "D": this._computeIntervalSize("D"),
408
+ "W": this._computeIntervalSize("W"),
409
+ "M": this._computeIntervalSize("M"),
410
+ "Y": this._computeIntervalSize("Y"),
411
+ "5Y": this._computeIntervalSize("5Y"),
412
+ "10Y": this._computeIntervalSize("10Y"),
413
+ }
414
+ }
415
+ _computeIntervalSize(scope) {
416
+ const maxDate = this.getMaxDateStr();
417
+ if (!maxDate) {
418
+ return 0;
419
+ }
420
+ const endDate = this._dateStrToUTCDate(maxDate);
421
+ const firstDayDateStr = this._getFirstDay(this._classify(this.getMinDateStr(), scope));
422
+ let currentDate = this._dateStrToUTCDate(firstDayDateStr);
423
+ let count = 0;
424
+ while (currentDate <= endDate) {
425
+ count++;
426
+ currentDate = this._increaseDateBy(scope, currentDate);
427
+ }
428
+ return count;
429
+ }
430
+ _increaseDateBy(scope, date) {
431
+ switch (scope) {
432
+ case "D":
433
+ return this._addDays(date, 1);
434
+ case "W":
435
+ return this._addDays(date, 7);
436
+ case "M":
437
+ return this._addMonths(date, 1);
438
+ case "Y":
439
+ return this._addYears(date, 1);
440
+ case "5Y":
441
+ return this._addYears(date, 5);
442
+ case "10Y":
443
+ return this._addYears(date, 10);
444
+ }
445
+ }
446
+
447
+ /*
448
+ * functions that add n days (_addDays), months (_addMonths)
449
+ * or years (_addYears) to a UTC date object
450
+ * returns the computed new UTC date
451
+ */
452
+ _addDays(UTCDate, days) {
453
+ let newUTCDate = new Date(UTCDate.valueOf());
454
+ newUTCDate.setUTCDate(newUTCDate.getUTCDate() + days);
455
+ return newUTCDate;
456
+ }
457
+ _addMonths(UTCdate, months) {
458
+ let newUTCDate = new Date(UTCdate.valueOf());
459
+ let d = newUTCDate.getUTCDate();
460
+ newUTCDate.setUTCMonth(newUTCDate.getUTCMonth() + +months);
461
+ if (newUTCDate.getUTCDate() != d) {
462
+ newUTCDate.setUTCDate(0);
463
+ }
464
+ return newUTCDate;
465
+ }
466
+ _addYears(UTCdate, years) {
467
+ let newUTCDate = new Date(UTCdate.valueOf());
468
+ newUTCDate.setUTCFullYear(newUTCDate.getUTCFullYear() + years);
469
+ return newUTCDate;
470
+ }
471
+
472
+ /*
473
+ * Validates dateStr. rules:
474
+ * => year: 4 digit number
475
+ * => month: [1-12]
476
+ * => day: [1-31]
477
+ */
478
+ _isValidDateStr(str) {
479
+ if (!str) {
480
+ return false;
481
+ }
482
+
483
+ let split = str.split("-");
484
+ if (split.length !== 3) return false;
485
+ let year = split[0];
486
+ let month = split[1];
487
+ let day = split[2];
488
+ if (year === "0000" || day === "00" || month === "00") return false;
489
+ if (Number(day) < 1 || Number(day) > 31) return false;
490
+ if (Number(month) < 1 || Number(month) > 12) return false;
491
+ // if all checks are passed => valid datestring!
492
+ return true;
493
+ }
494
+
495
+ /*
496
+ * Converts month number (str or number) to a 3 char
497
+ * abbreviation of the month (in english)
498
+ */
499
+ _monthLookup(num) {
500
+ if (num > 12 || num < 1) {
501
+ throw new Error(`invalid 'num' provided, expected 1-12. Got: ${num}`);
502
+ }
503
+ const lookup = {
504
+ "1": "Jan",
505
+ "2": "Feb",
506
+ "3": "Mar",
507
+ "4": "Apr",
508
+ "5": "May",
509
+ "6": "Jun",
510
+ "7": "Jul",
511
+ "8": "Aug",
512
+ "9": "Sep",
513
+ "10": "Oct",
514
+ "11": "Nov",
515
+ "12": "Dec",
516
+ }
517
+ return lookup[num.toString()];
518
+ }
519
+ }
520
+
521
+