1 import {Injectable, Pipe, PipeTransform} from '@angular/core';
2 import {DatePipe, DecimalPipe, getLocaleDateFormat, getLocaleTimeFormat, getLocaleDateTimeFormat, FormatWidth} from '@angular/common';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {LocaleService} from '@eg/core/locale.service';
7 import * as moment from 'moment-timezone';
8 import {DateUtil} from '@eg/share/util/date';
11 * Format IDL vield values for display.
16 export interface FormatParams {
21 orgField?: string; // 'shortname' || 'name'
22 datePlusTime?: boolean;
23 timezoneContextOrg?: number;
24 dateOnlyInterval?: string;
27 @Injectable({providedIn: 'root'})
28 export class FormatService {
30 dateFormat = 'shortDate';
31 dateTimeFormat = 'short';
32 wsOrgTimezone: string = OpenSRF.tz;
33 tzCache: {[orgId: number]: string} = {};
36 private datePipe: DatePipe,
37 private decimalPipe: DecimalPipe,
38 private idl: IdlService,
39 private org: OrgService,
40 private auth: AuthService,
41 private locale: LocaleService
44 // Create an inilne polyfill for Number.isNaN, which is
45 // not available in PhantomJS for unit testing.
46 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
48 // "The following works because NaN is the only value
49 // in javascript which is not equal to itself."
50 Number.isNaN = (value: any) => {
51 return value !== value;
57 * Create a human-friendly display version of any field type.
59 transform(params: FormatParams): string {
60 const value = params.value;
62 if ( value === undefined
65 || Number.isNaN(value)) {
69 let datatype = params.datatype;
72 if (params.idlClass && params.idlField) {
73 datatype = this.idl.classes[params.idlClass]
74 .field_map[params.idlField].datatype;
76 // Assume it's a primitive value
84 if (typeof value !== 'object') {
85 return value + ''; // no fleshed value here
88 if (!params.idlClass || !params.idlField) {
89 // Without a full accounting of the field data,
90 // we can't determine the linked selector field.
95 this.idl.getLinkSelector(params.idlClass, params.idlField);
97 if (selector && typeof value[selector] === 'function') {
98 const val = value[selector]();
100 if (Array.isArray(val)) {
101 // Typically has_many links will not be fleshed,
102 // but in the off-chance the are, avoid displaying
103 // an array reference value.
111 // We have an object with no display selector
112 // Display its pkey instead to avoid showing [object Object]
114 const pkey = this.idl.classes[params.idlClass].pkey;
115 if (pkey && typeof value[pkey] === 'function') {
116 return value[pkey]();
123 const orgField = params.orgField || 'shortname';
124 const org = this.org.get(value);
125 return org ? org[orgField]() : '';
129 if (params.idlField === 'dob') {
130 // special case: since dob is the only date column that the
131 // IDL thinks of as a timestamp, the date object comes over
132 // as a UTC value; apply the correct timezone rather than the
136 if (params.timezoneContextOrg) {
137 tz = this.getOrgTz( // support ID or object
138 this.org.get(params.timezoneContextOrg).id());
140 tz = this.wsOrgTimezone;
144 const date = moment(value).tz(tz);
145 if (!date || !date.isValid()) {
147 'Invalid date in format service; date=', value, 'tz=', tz);
151 let fmt = this.dateFormat || 'shortDate';
153 if (params.datePlusTime) {
154 // Time component directly requested
155 fmt = this.dateTimeFormat || 'short';
157 } else if (params.dateOnlyInterval) {
158 // Time component displays for non-day-granular intervals.
159 const secs = DateUtil.intervalToSeconds(params.dateOnlyInterval);
160 if (secs !== null && secs % 86400 !== 0) {
161 fmt = this.dateTimeFormat || 'short';
165 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
168 // TODO: this used to use CurrencyPipe, but that injected
169 // an assumption that the default currency is always going to be
170 // USD. Since CurrencyPipe doesn't have an apparent way to specify
171 // that that currency symbol shouldn't be displayed at all, it
172 // was switched to DecimalPipe
173 return this.decimalPipe.transform(value, '1.2-2');
176 // Slightly better than a bare 't' or 'f'.
177 // Note the caller is better off using an <eg-bool/> for
180 value === 't' || value === 1 ||
181 value === '1' || value === true
190 Fetch the org timezone from cache when available. Otherwise,
191 get the timezone from the org unit setting. The first time
192 this call is made, it may return the incorrect value since
193 it's not a promise-returning method (because format() is not a
194 promise-returning method). Future calls will return the correct
195 value since it's locally cached. Since most format() calls are
196 repeated many times for Angular digestion, the end result is that
197 the correct value will be used in the end.
199 getOrgTz(orgId: number): string {
201 if (this.tzCache[orgId] === null) {
202 // We are still waiting for the value to be returned
204 return this.wsOrgTimezone;
207 if (this.tzCache[orgId] !== undefined) {
208 // We have a cached value.
209 return this.tzCache[orgId];
212 // Avoid duplicate parallel lookups by indicating we
213 // are loading the value from the server.
214 this.tzCache[orgId] = null;
216 this.org.settings(['lib.timezone'], orgId)
217 .then(sets => this.tzCache[orgId] = sets['lib.timezone']);
219 // Use the local timezone while we wait for the real value
220 // to load from the server.
221 return this.wsOrgTimezone;
225 * Create an IDL-friendly display version of a human-readable date
227 idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
230 * Create an IDL-friendly display version of a human-readable datetime
232 idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
235 * Create a Moment from an ISO string
237 momentizeIsoString(isoString: string, timezone: string): moment.Moment {
238 return (isoString?.length) ? moment(isoString).tz(timezone) : moment();
242 * Turn a date string into a Moment using the date format org setting.
244 momentizeDateString(date: string, timezone: string, strict?, locale?): moment.Moment {
245 return this.momentize(date, this.makeFormatParseable(this.dateFormat, locale), timezone, strict);
249 * Turn a datetime string into a Moment using the datetime format org setting.
251 momentizeDateTimeString(date: string, timezone: string, strict?, locale?): moment.Moment {
252 return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat, locale), timezone, strict);
256 * Turn a string into a Moment using the provided format string.
258 private momentize(date: string, format: string, timezone: string, strict: boolean): moment.Moment {
260 const result = moment.tz(date, format, true, timezone);
261 if (!result.isValid()) {
263 throw new Error('Error parsing date ' + date);
265 return moment.tz(date, format, false, timezone);
267 return moment(new Date(date), timezone);
272 * Takes a dateFormat or dateTimeFormat string (which uses Angular syntax) and transforms
273 * it into a format string that MomentJs can use to parse input human-readable strings
274 * (https://momentjs.com/docs/#/parsing/string-format/)
276 * Returns a blank string if it can't do this transformation.
278 private makeFormatParseable(original: string, locale?: string): string {
279 if (!original) { return ''; }
280 if (!locale) { locale = this.locale.currentLocaleCode(); }
283 const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
284 const date = getLocaleDateFormat(locale, FormatWidth.Short);
285 const time = getLocaleTimeFormat(locale, FormatWidth.Short);
287 .replace('{1}', date)
288 .replace('{0}', time)
289 .replace(/\'(\w+)\'/, '[$1]');
293 const template = getLocaleDateTimeFormat(locale, FormatWidth.Medium);
294 const date = getLocaleDateFormat(locale, FormatWidth.Medium);
295 const time = getLocaleTimeFormat(locale, FormatWidth.Medium);
297 .replace('{1}', date)
298 .replace('{0}', time)
299 .replace(/\'(\w+)\'/, '[$1]');
303 const template = getLocaleDateTimeFormat(locale, FormatWidth.Long);
304 const date = getLocaleDateFormat(locale, FormatWidth.Long);
305 const time = getLocaleTimeFormat(locale, FormatWidth.Long);
307 .replace('{1}', date)
308 .replace('{0}', time)
309 .replace(/\'(\w+)\'/, '[$1]');
313 const template = getLocaleDateTimeFormat(locale, FormatWidth.Full);
314 const date = getLocaleDateFormat(locale, FormatWidth.Full);
315 const time = getLocaleTimeFormat(locale, FormatWidth.Full);
317 .replace('{1}', date)
318 .replace('{0}', time)
319 .replace(/\'(\w+)\'/, '[$1]');
323 original = getLocaleDateFormat(locale, FormatWidth.Short);
327 original = getLocaleDateFormat(locale, FormatWidth.Medium);
331 original = getLocaleDateFormat(locale, FormatWidth.Long);
335 original = getLocaleDateFormat(locale, FormatWidth.Full);
339 original = getLocaleTimeFormat(locale, FormatWidth.Short);
343 original = getLocaleTimeFormat(locale, FormatWidth.Medium);
347 original = getLocaleTimeFormat(locale, FormatWidth.Long);
351 original = getLocaleTimeFormat(locale, FormatWidth.Full);
356 .replace(/a+/g, 'a') // MomentJs can handle all sorts of meridian strings
357 .replace(/d/g, 'D') // MomentJs capitalizes day of month
358 .replace(/EEEEEE/g, '') // MomentJs does not handle short day of week
359 .replace(/EEEEE/g, '') // MomentJs does not handle narrow day of week
360 .replace(/EEEE/g, 'dddd') // MomentJs has different syntax for long day of week
361 .replace(/E{1,3}/g, 'ddd') // MomentJs has different syntax for abbreviated day of week
362 .replace(/L/g, 'M') // MomentJs does not differentiate between month and month standalone
363 .replace(/W/g, '') // MomentJs uses W for something else
364 .replace(/y/g, 'Y') // MomentJs capitalizes year
365 .replace(/ZZZZ|z{1,4}/g, '[GMT]Z') // MomentJs doesn't put "UTC" in front of offset
366 .replace(/Z{2,3}/g, 'Z'); // MomentJs only uses 1 Z
371 // Pipe-ify the above formating logic for use in templates
372 @Pipe({name: 'formatValue'})
373 export class FormatValuePipe implements PipeTransform {
374 constructor(private formatter: FormatService) {}
375 // Add other filter params as needed to fill in the FormatParams
376 transform(value: string, datatype: string): string {
377 return this.formatter.transform({value: value, datatype: datatype});
381 @Pipe({name: 'egOrgDateInContext'})
382 export class OrgDateInContextPipe implements PipeTransform {
383 constructor(private formatter: FormatService) {}
385 transform(value: string, orgId?: number, interval?: string ): string {
386 return this.formatter.transform({
388 datatype: 'timestamp',
389 timezoneContextOrg: orgId,
390 dateOnlyInterval: interval
395 @Pipe({name: 'egDueDate'})
396 export class DueDatePipe implements PipeTransform {
397 constructor(private formatter: FormatService) {}
399 transform(circ: IdlObject): string {
400 return this.formatter.transform({
401 value: circ.due_date(),
402 datatype: 'timestamp',
403 timezoneContextOrg: circ.circ_lib(),
404 dateOnlyInterval: circ.duration()