/* * Copyright (C) 2020-2021 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import moment from 'moment'; import {Subscription} from 'rxjs'; import {logger} from '../_helpers/ts-logger'; // TODO: use import for opening_hours when the change is published with a new update // see https://github.com/opening-hours/opening_hours.js/pull/407 // eslint-disable-next-line @typescript-eslint/no-var-requires, unicorn/prefer-module const ohFunction = require('opening_hours'); @Injectable() @Pipe({ name: 'join', pure: true, }) export class ArrayJoinPipe implements PipeTransform { value = ''; transform(anArray: unknown[] | unknown, separator: string | unknown): string { if (typeof separator !== 'string' || separator.length <= 0) { return this.value; } if (!Array.isArray(anArray)) { throw new SyntaxError( `Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`, ); } this.value = anArray.join(separator); return this.value; } } @Injectable() @Pipe({ name: 'sentencecase', pure: true, }) export class SentenceCasePipe implements PipeTransform { value = ''; transform(aString: string | unknown): string { if (typeof aString !== 'string') { throw new SyntaxError( `Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`, ); } this.value = aString.slice(0, 1).toUpperCase() + aString.slice(1); return this.value; } } @Injectable() @Pipe({ name: 'split', pure: true, }) export class StringSplitPipe implements PipeTransform { value = new Array(); transform(aString: string | unknown, splitter: string | unknown): unknown[] { if (typeof splitter !== 'string' || splitter.length <= 0) { return this.value as never; } if (typeof aString !== 'string') { throw new SyntaxError( `Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`, ); } this.value = aString.split(splitter); return this.value as never; } } @Injectable() @Pipe({ name: 'openingHours', pure: true, }) export class OpeningHoursPipe implements PipeTransform, OnDestroy { locale: string; onLangChange?: Subscription; value = ''; constructor(private readonly translate: TranslateService) { this.locale = translate.currentLang; } private _dispose(): void { if (this.onLangChange?.closed === false) { this.onLangChange?.unsubscribe(); } } ngOnDestroy(): void { this._dispose(); } transform(aString: string | unknown): string { this.updateValue(aString); this._dispose(); if (this.onLangChange?.closed === true) { this.onLangChange = this.translate.onLangChange.subscribe( (event: LangChangeEvent) => { this.locale = event.lang; this.updateValue(aString); }, ); } return this.value; } updateValue(aString: string | unknown) { if (typeof aString !== 'string') { logger.warn(`openingHours pipe unable to parse input: ${aString}`); return; } let openingHours; try { openingHours = new ohFunction(aString, { address: { country_code: 'de', state: 'Hessen', }, lon: 8.667_97, lat: 50.129_16, }); } catch (error) { logger.warn(error); this.value = ''; return; } const isOpen: boolean = openingHours.getState(); const nextChange: Date = openingHours.getNextChange(); let prefixKey = isOpen ? 'common.openingHours.open_until' : 'common.openingHours.closed_until'; let formattedCalender = moment(nextChange).calendar(); if (moment(nextChange).isBefore(moment().add(1, 'hours'))) { prefixKey = isOpen ? 'common.openingHours.closing_soon' : 'common.openingHours.opening_soon'; formattedCalender = formattedCalender.slice(0, 1).toUpperCase() + formattedCalender.slice(1); } this.value = `${this.translate.instant(prefixKey)} ${formattedCalender}`; } } @Injectable() @Pipe({ name: 'durationLocalized', pure: true, }) export class DurationLocalizedPipe implements PipeTransform, OnDestroy { locale: string; onLangChange?: Subscription; value: string; frequencyPrefixes: {[iso6391Code: string]: string} = { de: 'alle', en: 'every', es: 'cada', pt: 'a cada', fr: 'tous les', cn: '每', ru: 'kаждые', }; constructor(private readonly translate: TranslateService) { this.locale = translate.currentLang; } private _dispose(): void { if (this.onLangChange?.closed === false) { this.onLangChange?.unsubscribe(); } } ngOnDestroy(): void { this._dispose(); } /** * @param value An ISO 8601 duration string * @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency */ transform(value: string | unknown, isFrequency = false): string { this.updateValue(value, isFrequency); this._dispose(); if (this.onLangChange?.closed === true) { this.onLangChange = this.translate.onLangChange.subscribe( (event: LangChangeEvent) => { this.locale = event.lang; this.updateValue(value, isFrequency); }, ); } return this.value; } updateValue(value: string | unknown, isFrequency = false): void { if (typeof value !== 'string') { logger.warn(`durationLocalized pipe unable to parse input: ${value}`); return; } if (isFrequency) { const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter( element => this.locale.includes(element), ); this.value = [ fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en, moment.duration(value).humanize(), ].join(' '); } else { this.value = moment.duration(value).humanize(); } } } @Injectable() @Pipe({ name: 'metersLocalized', pure: false, }) export class MetersLocalizedPipe implements PipeTransform, OnDestroy { locale: string; onLangChange?: Subscription; value = ''; constructor(private readonly translate: TranslateService) { this.locale = translate.currentLang; } private _dispose(): void { if (this.onLangChange?.closed === false) { this.onLangChange?.unsubscribe(); } } ngOnDestroy(): void { this._dispose(); } transform(value: string | number | unknown): string { this.updateValue(value); this._dispose(); if (this.onLangChange?.closed === true) { this.onLangChange = this.translate.onLangChange.subscribe( (event: LangChangeEvent) => { this.locale = event.lang; this.updateValue(value); }, ); } return this.value; } updateValue(value: string | number | unknown) { if (typeof value !== 'string' && typeof value !== 'number') { logger.warn(`metersLocalized pipe unable to parse input: ${value}`); return; } const imperialLocale = ['US', 'UK', 'LR', 'MM'].some(term => this.locale.includes(term), ); const meters = typeof value === 'string' ? Number.parseFloat(value) : (value as number); if (imperialLocale) { const yards = meters * 1.0936; const options = { style: 'unit', unit: yards >= 1760 ? 'mile' : 'yard', maximumFractionDigits: yards >= 1760 ? 1 : 0, } as unknown as Intl.NumberFormatOptions; this.value = new Intl.NumberFormat(this.locale, options).format( yards >= 1760 ? yards / 1760 : yards, ); } else { const options = { style: 'unit', unit: meters >= 1000 ? 'kilometer' : 'meter', maximumFractionDigits: meters >= 1000 ? 1 : 0, } as unknown as Intl.NumberFormatOptions; this.value = new Intl.NumberFormat(this.locale, options).format( meters >= 1000 ? meters / 1000 : meters, ); } } } @Injectable() @Pipe({ name: 'numberLocalized', pure: true, }) export class NumberLocalizedPipe implements PipeTransform, OnDestroy { locale: string; onLangChange?: Subscription; value: string; constructor(private readonly translate: TranslateService) { this.locale = translate.currentLang; } private _dispose(): void { if (this.onLangChange?.closed === false) { this.onLangChange?.unsubscribe(); } } ngOnDestroy(): void { this._dispose(); } /** * @param value The number to be formatted * @param formatOptions Formatting options to include. * As specified by Intl.NumberFormatOptions as comma seperated key:value pairs. */ transform(value: string | number | unknown, formatOptions?: string): string { this.updateValue(value, formatOptions); this._dispose(); if (this.onLangChange?.closed === true) { this.onLangChange = this.translate.onLangChange.subscribe( (event: LangChangeEvent) => { this.locale = event.lang; this.updateValue(value, formatOptions); }, ); } return this.value; } updateValue(value: string | number | unknown, formatOptions?: string): void { if (typeof value !== 'string' && typeof value !== 'number') { logger.warn(`numberLocalized pipe unable to parse input: ${value}`); return; } const options = formatOptions ?.split(',') .map(element => element.split(':')) // eslint-disable-next-line unicorn/no-array-reduce .reduce( (accumulator, [key, value_]) => ({ ...accumulator, [key.trim()]: value_.trim(), }), {}, ) as Intl.NumberFormatOptions; const float = typeof value === 'string' ? Number.parseFloat(value) : (value as number); this.value = new Intl.NumberFormat(this.locale, options).format(float); } } @Injectable() @Pipe({ name: 'dateFormat', pure: true, }) export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy { locale: string; onLangChange?: Subscription; value: string; constructor(private readonly translate: TranslateService) { this.locale = translate.currentLang; } private _dispose(): void { if (this.onLangChange?.closed === false) { this.onLangChange?.unsubscribe(); } } ngOnDestroy(): void { this._dispose(); } /** * @param value The date to be formatted * @param formatOptions Dateformat options to include. * As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs * Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35" */ transform(value: string | unknown, formatOptions?: string): string { this.updateValue(value, formatOptions); this._dispose(); if (this.onLangChange?.closed === true) { this.onLangChange = this.translate.onLangChange.subscribe( (event: LangChangeEvent) => { this.locale = event.lang; this.updateValue(value, formatOptions); }, ); } return this.value; } updateValue(value: string | Date | unknown, formatOptions?: string): void { if ( typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]' ) { logger.warn(`dateFormat pipe unable to parse input: ${value}`); return; } const options = formatOptions ?.split(',') .map(element => element.split(':')) // eslint-disable-next-line unicorn/no-array-reduce .reduce( (accumulator, [key, value_]) => ({ ...accumulator, [key.trim()]: value_.trim(), }), {}, ) as Intl.DateTimeFormatOptions; const date = typeof value === 'string' ? Date.parse(value) : (value as Date); this.value = new Intl.DateTimeFormat( this.locale, options ?? { day: 'numeric', month: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', }, ).format(date); } }