1 import {Injectable, Pipe, PipeTransform} from '@angular/core';
2 import {DatePipe, CurrencyPipe} from '@angular/common';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import * as Moment from 'moment-timezone';
8 * Format IDL vield values for display.
13 export interface FormatParams {
18 orgField?: string; // 'shortname' || 'name'
19 datePlusTime?: boolean;
20 timezoneContextOrg?: number;
23 @Injectable({providedIn: 'root'})
24 export class FormatService {
26 dateFormat = 'shortDate';
27 dateTimeFormat = 'short';
28 wsOrgTimezone: string = OpenSRF.tz;
31 private datePipe: DatePipe,
32 private currencyPipe: CurrencyPipe,
33 private idl: IdlService,
34 private org: OrgService
37 // Create an inilne polyfill for Number.isNaN, which is
38 // not available in PhantomJS for unit testing.
39 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
41 // "The following works because NaN is the only value
42 // in javascript which is not equal to itself."
43 Number.isNaN = (value: any) => {
44 return value !== value;
50 * Create a human-friendly display version of any field type.
52 transform(params: FormatParams): string {
53 const value = params.value;
55 if ( value === undefined
58 || Number.isNaN(value)) {
62 let datatype = params.datatype;
65 if (params.idlClass && params.idlField) {
66 datatype = this.idl.classes[params.idlClass]
67 .field_map[params.idlField].datatype;
69 // Assume it's a primitive value
77 if (typeof value !== 'object') {
78 return value + ''; // no fleshed value here
81 if (!params.idlClass || !params.idlField) {
82 // Without a full accounting of the field data,
83 // we can't determine the linked selector field.
88 this.idl.getLinkSelector(params.idlClass, params.idlField);
90 if (selector && typeof value[selector] === 'function') {
91 const val = value[selector]();
93 if (Array.isArray(val)) {
94 // Typically has_many links will not be fleshed,
95 // but in the off-chance the are, avoid displaying
96 // an array reference value.
107 const orgField = params.orgField || 'shortname';
108 const org = this.org.get(value);
109 return org ? org[orgField]() : '';
113 if (params.idlField === 'dob') {
114 // special case: since dob is the only date column that the
115 // IDL thinks of as a timestamp, the date object comes over
116 // as a UTC value; apply the correct timezone rather than the
120 tz = this.wsOrgTimezone;
122 const date = Moment(value).tz(tz);
123 if (!date.isValid()) {
124 console.error('Invalid date in format service', value);
127 let fmt = this.dateFormat || 'shortDate';
128 if (params.datePlusTime) {
129 fmt = this.dateTimeFormat || 'short';
131 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
134 return this.currencyPipe.transform(value);
137 // Slightly better than a bare 't' or 'f'.
138 // Note the caller is better off using an <eg-bool/> for
141 value === 't' || value === 1 ||
142 value === '1' || value === true
150 * Create an IDL-friendly display version of a human-readable date
152 idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
155 * Create an IDL-friendly display version of a human-readable datetime
157 idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
160 * Turn a date string into a Moment using the date format org setting.
162 momentizeDateString(date: string, timezone: string, strict = false): Moment {
163 return this.momentize(date, this.makeFormatParseable(this.dateFormat), timezone, strict);
167 * Turn a datetime string into a Moment using the datetime format org setting.
169 momentizeDateTimeString(date: string, timezone: string, strict = false): Moment {
170 return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat), timezone, strict);
174 * Turn a string into a Moment using the provided format string.
176 private momentize(date: string, format: string, timezone: string, strict: boolean): Moment {
178 const result = Moment.tz(date, format, true, timezone);
179 if (isNaN(result) || 'Invalid date' === result) {
181 throw new Error('Error parsing date ' + date);
183 return Moment.tz(date, format, false, timezone);
185 // TODO: The following fallback returns the date at midnight UTC,
186 // rather than midnight in the local TZ
187 return Moment.tz(date, timezone);
192 * Takes a dateFormat or dateTimeFormat string (which uses Angular syntax) and transforms
193 * it into a format string that MomentJs can use to parse input human-readable strings
194 * (https://momentjs.com/docs/#/parsing/string-format/)
196 * Returns a blank string if it can't do this transformation.
198 private makeFormatParseable(original: string): string {
199 if (!original) { return ''; }
202 return 'M/D/YY, h:mm a';
205 return 'MMM D, Y, h:mm:ss a';
208 return 'MMMM D, Y, h:mm:ss a [GMT]Z';
211 return 'dddd, MMMM D, Y, h:mm:ss a [GMT]Z';
223 return 'dddd, MMMM D, Y';
232 return 'h:mm:ss a [GMT]Z';
235 return 'h:mm:ss a [GMT]Z';
239 .replace(/a+/g, 'a') // MomentJs can handle all sorts of meridian strings
240 .replace(/d/g, 'D') // MomentJs capitalizes day of month
241 .replace(/EEEEEE/g, '') // MomentJs does not handle short day of week
242 .replace(/EEEEE/g, '') // MomentJs does not handle narrow day of week
243 .replace(/EEEE/g, 'dddd') // MomentJs has different syntax for long day of week
244 .replace(/E{1,3}/g, 'ddd') // MomentJs has different syntax for abbreviated day of week
245 .replace(/L/g, 'M') // MomentJs does not differentiate between month and month standalone
246 .replace(/W/g, '') // MomentJs uses W for something else
247 .replace(/y/g, 'Y') // MomentJs capitalizes year
248 .replace(/ZZZZ|z{1,4}/g, '[GMT]Z') // MomentJs doesn't put "UTC" in front of offset
249 .replace(/Z{2,3}/g, 'Z'); // MomentJs only uses 1 Z
254 // Pipe-ify the above formating logic for use in templates
255 @Pipe({name: 'formatValue'})
256 export class FormatValuePipe implements PipeTransform {
257 constructor(private formatter: FormatService) {}
258 // Add other filter params as needed to fill in the FormatParams
259 transform(value: string, datatype: string): string {
260 return this.formatter.transform({value: value, datatype: datatype});