1 import {Injectable, Pipe, PipeTransform} from '@angular/core';
2 import {DatePipe, CurrencyPipe, 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 {LocaleService} from '@eg/core/locale.service';
6 import * as Moment from 'moment-timezone';
9 * Format IDL vield values for display.
14 export interface FormatParams {
19 orgField?: string; // 'shortname' || 'name'
20 datePlusTime?: boolean;
21 timezoneContextOrg?: number;
24 @Injectable({providedIn: 'root'})
25 export class FormatService {
27 dateFormat = 'shortDate';
28 dateTimeFormat = 'short';
29 wsOrgTimezone: string = OpenSRF.tz;
32 private datePipe: DatePipe,
33 private currencyPipe: CurrencyPipe,
34 private idl: IdlService,
35 private org: OrgService,
36 private locale: LocaleService
39 // Create an inilne polyfill for Number.isNaN, which is
40 // not available in PhantomJS for unit testing.
41 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
43 // "The following works because NaN is the only value
44 // in javascript which is not equal to itself."
45 Number.isNaN = (value: any) => {
46 return value !== value;
52 * Create a human-friendly display version of any field type.
54 transform(params: FormatParams): string {
55 const value = params.value;
57 if ( value === undefined
60 || Number.isNaN(value)) {
64 let datatype = params.datatype;
67 if (params.idlClass && params.idlField) {
68 datatype = this.idl.classes[params.idlClass]
69 .field_map[params.idlField].datatype;
71 // Assume it's a primitive value
79 if (typeof value !== 'object') {
80 return value + ''; // no fleshed value here
83 if (!params.idlClass || !params.idlField) {
84 // Without a full accounting of the field data,
85 // we can't determine the linked selector field.
90 this.idl.getLinkSelector(params.idlClass, params.idlField);
92 if (selector && typeof value[selector] === 'function') {
93 const val = value[selector]();
95 if (Array.isArray(val)) {
96 // Typically has_many links will not be fleshed,
97 // but in the off-chance the are, avoid displaying
98 // an array reference value.
109 const orgField = params.orgField || 'shortname';
110 const org = this.org.get(value);
111 return org ? org[orgField]() : '';
115 if (params.idlField === 'dob') {
116 // special case: since dob is the only date column that the
117 // IDL thinks of as a timestamp, the date object comes over
118 // as a UTC value; apply the correct timezone rather than the
122 tz = this.wsOrgTimezone;
124 const date = Moment(value).tz(tz);
125 if (!date.isValid()) {
126 console.error('Invalid date in format service', value);
129 let fmt = this.dateFormat || 'shortDate';
130 if (params.datePlusTime) {
131 fmt = this.dateTimeFormat || 'short';
133 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
136 return this.currencyPipe.transform(value);
139 // Slightly better than a bare 't' or 'f'.
140 // Note the caller is better off using an <eg-bool/> for
143 value === 't' || value === 1 ||
144 value === '1' || value === true
152 * Create an IDL-friendly display version of a human-readable date
154 idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
157 * Create an IDL-friendly display version of a human-readable datetime
159 idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
162 * Create a Moment from an ISO string
164 momentizeIsoString(isoString: string, timezone: string): Moment {
165 return (isoString.length) ? Moment(isoString, timezone) : Moment();
169 * Turn a date string into a Moment using the date format org setting.
171 momentizeDateString(date: string, timezone: string, strict?, locale?): Moment {
172 return this.momentize(date, this.makeFormatParseable(this.dateFormat, locale), timezone, strict);
176 * Turn a datetime string into a Moment using the datetime format org setting.
178 momentizeDateTimeString(date: string, timezone: string, strict?, locale?): Moment {
179 return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat, locale), timezone, strict);
183 * Turn a string into a Moment using the provided format string.
185 private momentize(date: string, format: string, timezone: string, strict: boolean): Moment {
187 const result = Moment.tz(date, format, true, timezone);
188 if (isNaN(result) || 'Invalid date' === result) {
190 throw new Error('Error parsing date ' + date);
192 return Moment.tz(date, format, false, timezone);
194 return Moment(new Date(date), timezone);
199 * Takes a dateFormat or dateTimeFormat string (which uses Angular syntax) and transforms
200 * it into a format string that MomentJs can use to parse input human-readable strings
201 * (https://momentjs.com/docs/#/parsing/string-format/)
203 * Returns a blank string if it can't do this transformation.
205 private makeFormatParseable(original: string, locale?: string): string {
206 if (!original) { return ''; }
207 if (!locale) { locale = this.locale.currentLocaleCode(); }
210 const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
211 const date = getLocaleDateFormat(locale, FormatWidth.Short);
212 const time = getLocaleTimeFormat(locale, FormatWidth.Short);
214 .replace('{1}', date)
215 .replace('{0}', time)
216 .replace(/\'(\w+)\'/, '[$1]');
220 const template = getLocaleDateTimeFormat(locale, FormatWidth.Medium);
221 const date = getLocaleDateFormat(locale, FormatWidth.Medium);
222 const time = getLocaleTimeFormat(locale, FormatWidth.Medium);
224 .replace('{1}', date)
225 .replace('{0}', time)
226 .replace(/\'(\w+)\'/, '[$1]');
230 const template = getLocaleDateTimeFormat(locale, FormatWidth.Long);
231 const date = getLocaleDateFormat(locale, FormatWidth.Long);
232 const time = getLocaleTimeFormat(locale, FormatWidth.Long);
234 .replace('{1}', date)
235 .replace('{0}', time)
236 .replace(/\'(\w+)\'/, '[$1]');
240 const template = getLocaleDateTimeFormat(locale, FormatWidth.Full);
241 const date = getLocaleDateFormat(locale, FormatWidth.Full);
242 const time = getLocaleTimeFormat(locale, FormatWidth.Full);
244 .replace('{1}', date)
245 .replace('{0}', time)
246 .replace(/\'(\w+)\'/, '[$1]');
250 original = getLocaleDateFormat(locale, FormatWidth.Short);
254 original = getLocaleDateFormat(locale, FormatWidth.Medium);
258 original = getLocaleDateFormat(locale, FormatWidth.Long);
262 original = getLocaleDateFormat(locale, FormatWidth.Full);
266 original = getLocaleTimeFormat(locale, FormatWidth.Short);
270 original = getLocaleTimeFormat(locale, FormatWidth.Medium);
274 original = getLocaleTimeFormat(locale, FormatWidth.Long);
278 original = getLocaleTimeFormat(locale, FormatWidth.Full);
283 .replace(/a+/g, 'a') // MomentJs can handle all sorts of meridian strings
284 .replace(/d/g, 'D') // MomentJs capitalizes day of month
285 .replace(/EEEEEE/g, '') // MomentJs does not handle short day of week
286 .replace(/EEEEE/g, '') // MomentJs does not handle narrow day of week
287 .replace(/EEEE/g, 'dddd') // MomentJs has different syntax for long day of week
288 .replace(/E{1,3}/g, 'ddd') // MomentJs has different syntax for abbreviated day of week
289 .replace(/L/g, 'M') // MomentJs does not differentiate between month and month standalone
290 .replace(/W/g, '') // MomentJs uses W for something else
291 .replace(/y/g, 'Y') // MomentJs capitalizes year
292 .replace(/ZZZZ|z{1,4}/g, '[GMT]Z') // MomentJs doesn't put "UTC" in front of offset
293 .replace(/Z{2,3}/g, 'Z'); // MomentJs only uses 1 Z
298 // Pipe-ify the above formating logic for use in templates
299 @Pipe({name: 'formatValue'})
300 export class FormatValuePipe implements PipeTransform {
301 constructor(private formatter: FormatService) {}
302 // Add other filter params as needed to fill in the FormatParams
303 transform(value: string, datatype: string): string {
304 return this.formatter.transform({value: value, datatype: datatype});