]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/format.service.ts
b6fba2dea4f38ad81454450e4238a821675f2ad5
[Evergreen.git] / Open-ILS / src / eg2 / src / app / core / format.service.ts
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';
7
8 /**
9  * Format IDL vield values for display.
10  */
11
12 declare var OpenSRF;
13
14 export interface FormatParams {
15     value: any;
16     idlClass?: string;
17     idlField?: string;
18     datatype?: string;
19     orgField?: string; // 'shortname' || 'name'
20     datePlusTime?: boolean;
21     timezoneContextOrg?: number;
22 }
23
24 @Injectable({providedIn: 'root'})
25 export class FormatService {
26
27     dateFormat = 'shortDate';
28     dateTimeFormat = 'short';
29     wsOrgTimezone: string = OpenSRF.tz;
30
31     constructor(
32         private datePipe: DatePipe,
33         private currencyPipe: CurrencyPipe,
34         private idl: IdlService,
35         private org: OrgService,
36         private locale: LocaleService
37     ) {
38
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
42         if (!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;
47             };
48         }
49     }
50
51     /**
52      * Create a human-friendly display version of any field type.
53      */
54     transform(params: FormatParams): string {
55         const value = params.value;
56
57         if (   value === undefined
58             || value === null
59             || value === ''
60             || Number.isNaN(value)) {
61             return '';
62         }
63
64         let datatype = params.datatype;
65
66         if (!datatype) {
67             if (params.idlClass && params.idlField) {
68                 datatype = this.idl.classes[params.idlClass]
69                     .field_map[params.idlField].datatype;
70             } else {
71                 // Assume it's a primitive value
72                 return value + '';
73             }
74         }
75
76         switch (datatype) {
77
78             case 'link':
79                 if (typeof value !== 'object') {
80                     return value + ''; // no fleshed value here
81                 }
82
83                 if (!params.idlClass || !params.idlField) {
84                     // Without a full accounting of the field data,
85                     // we can't determine the linked selector field.
86                     return value + '';
87                 }
88
89                 const selector =
90                     this.idl.getLinkSelector(params.idlClass, params.idlField);
91
92                 if (selector && typeof value[selector] === 'function') {
93                     const val = value[selector]();
94
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.
99                         return '';
100                     } else {
101                         return val + '';
102                     }
103
104                 } else {
105                     return value + '';
106                 }
107
108             case 'org_unit':
109                 const orgField = params.orgField || 'shortname';
110                 const org = this.org.get(value);
111                 return org ? org[orgField]() : '';
112
113             case 'timestamp':
114                 let tz;
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
119                     // local one
120                     tz = 'UTC';
121                 } else {
122                     tz = this.wsOrgTimezone;
123                 }
124                 const date = Moment(value).tz(tz);
125                 if (!date.isValid()) {
126                     console.error('Invalid date in format service', value);
127                     return '';
128                 }
129                 let fmt = this.dateFormat || 'shortDate';
130                 if (params.datePlusTime) {
131                     fmt = this.dateTimeFormat || 'short';
132                 }
133                 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
134
135             case 'money':
136                 return this.currencyPipe.transform(value);
137
138             case 'bool':
139                 // Slightly better than a bare 't' or 'f'.
140                 // Note the caller is better off using an <eg-bool/> for
141                 // boolean display.
142                 return Boolean(
143                     value === 't' || value === 1 ||
144                     value === '1' || value === true
145                 ).toString();
146
147             default:
148                 return value + '';
149         }
150     }
151     /**
152      * Create an IDL-friendly display version of a human-readable date
153      */
154     idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
155
156     /**
157      * Create an IDL-friendly display version of a human-readable datetime
158      */
159     idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
160
161     /**
162      * Turn a date string into a Moment using the date format org setting.
163      */
164     momentizeDateString(date: string, timezone: string, strict = false): Moment {
165         return this.momentize(date, this.makeFormatParseable(this.dateFormat), timezone, strict);
166     }
167
168     /**
169      * Turn a datetime string into a Moment using the datetime format org setting.
170      */
171     momentizeDateTimeString(date: string, timezone: string, strict = false): Moment {
172         return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat), timezone, strict);
173     }
174
175     /**
176      * Turn a string into a Moment using the provided format string.
177      */
178     private momentize(date: string, format: string, timezone: string, strict: boolean): Moment {
179         if (format.length) {
180             const result = Moment.tz(date, format, true, timezone);
181             if (isNaN(result) || 'Invalid date' === result) {
182                 if (strict) {
183                     throw new Error('Error parsing date ' + date);
184                 }
185                 return Moment.tz(date, format, false, timezone);
186             }
187         // TODO: The following fallback returns the date at midnight UTC,
188         // rather than midnight in the local TZ
189         return Moment.tz(date, timezone);
190         }
191     }
192
193     /**
194      * Takes a dateFormat or dateTimeFormat string (which uses Angular syntax) and transforms
195      * it into a format string that MomentJs can use to parse input human-readable strings
196      * (https://momentjs.com/docs/#/parsing/string-format/)
197      *
198      * Returns a blank string if it can't do this transformation.
199      */
200     private makeFormatParseable(original: string, locale?: string): string {
201         if (!original) { return ''; }
202         if (!locale) { locale = locale; }
203         switch (original) {
204             case 'short': {
205                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
206                 const date = getLocaleDateFormat(locale, FormatWidth.Short);
207                 const time = getLocaleTimeFormat(locale, FormatWidth.Short);
208                 original = template
209                     .replace('{1}', date)
210                     .replace('{0}', time)
211                     .replace(/\'(\w+)\'/, '[$1]');
212                 break;
213             }
214             case 'medium': {
215                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Medium);
216                 const date = getLocaleDateFormat(locale, FormatWidth.Medium);
217                 const time = getLocaleTimeFormat(locale, FormatWidth.Medium);
218                 original = template
219                     .replace('{1}', date)
220                     .replace('{0}', time)
221                     .replace(/\'(\w+)\'/, '[$1]');
222                 break;
223             }
224             case 'long': {
225                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Long);
226                 const date = getLocaleDateFormat(locale, FormatWidth.Long);
227                 const time = getLocaleTimeFormat(locale, FormatWidth.Long);
228                 original = template
229                     .replace('{1}', date)
230                     .replace('{0}', time)
231                     .replace(/\'(\w+)\'/, '[$1]');
232                 break;
233             }
234             case 'full': {
235                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Full);
236                 const date = getLocaleDateFormat(locale, FormatWidth.Full);
237                 const time = getLocaleTimeFormat(locale, FormatWidth.Full);
238                 original = template
239                     .replace('{1}', date)
240                     .replace('{0}', time)
241                     .replace(/\'(\w+)\'/, '[$1]');
242                 break;
243             }
244             case 'shortDate': {
245                 original = getLocaleDateFormat(locale, FormatWidth.Short);
246                 break;
247             }
248             case 'mediumDate': {
249                 original = getLocaleDateFormat(locale, FormatWidth.Medium);
250                 break;
251             }
252             case 'longDate': {
253                 original = getLocaleDateFormat(locale, FormatWidth.Long);
254                 break;
255             }
256             case 'fullDate': {
257                 original = getLocaleDateFormat(locale, FormatWidth.Full);
258                 break;
259             }
260             case 'shortTime': {
261                 original = getLocaleTimeFormat(locale, FormatWidth.Short);
262                 break;
263             }
264             case 'mediumTime': {
265                 original = getLocaleTimeFormat(locale, FormatWidth.Medium);
266                 break;
267             }
268             case 'longTime': {
269                 original = getLocaleTimeFormat(locale, FormatWidth.Long);
270                 break;
271             }
272             case 'fullTime': {
273                 original = getLocaleTimeFormat(locale, FormatWidth.Full);
274                 break;
275             }
276         }
277         return original
278             .replace(/a+/g, 'a') // MomentJs can handle all sorts of meridian strings
279             .replace(/d/g, 'D') // MomentJs capitalizes day of month
280             .replace(/EEEEEE/g, '') // MomentJs does not handle short day of week
281             .replace(/EEEEE/g, '') // MomentJs does not handle narrow day of week
282             .replace(/EEEE/g, 'dddd') // MomentJs has different syntax for long day of week
283             .replace(/E{1,3}/g, 'ddd') // MomentJs has different syntax for abbreviated day of week
284             .replace(/L/g, 'M') // MomentJs does not differentiate between month and month standalone
285             .replace(/W/g, '') // MomentJs uses W for something else
286             .replace(/y/g, 'Y') // MomentJs capitalizes year
287             .replace(/ZZZZ|z{1,4}/g, '[GMT]Z') // MomentJs doesn't put "UTC" in front of offset
288             .replace(/Z{2,3}/g, 'Z'); // MomentJs only uses 1 Z
289     }
290 }
291
292
293 // Pipe-ify the above formating logic for use in templates
294 @Pipe({name: 'formatValue'})
295 export class FormatValuePipe implements PipeTransform {
296     constructor(private formatter: FormatService) {}
297     // Add other filter params as needed to fill in the FormatParams
298     transform(value: string, datatype: string): string {
299         return this.formatter.transform({value: value, datatype: datatype});
300     }
301 }