/*
* 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);
}
}