Forward-port 3.5.0 upgrade script
[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      * Create a Moment from an ISO string
163      */
164     momentizeIsoString(isoString: string, timezone: string): moment.Moment {
165         return (isoString.length) ? moment(isoString, timezone) : moment();
166     }
167
168     /**
169      * Turn a date string into a Moment using the date format org setting.
170      */
171     momentizeDateString(date: string, timezone: string, strict?, locale?): moment.Moment {
172         return this.momentize(date, this.makeFormatParseable(this.dateFormat, locale), timezone, strict);
173     }
174
175     /**
176      * Turn a datetime string into a Moment using the datetime format org setting.
177      */
178     momentizeDateTimeString(date: string, timezone: string, strict?, locale?): moment.Moment {
179         return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat, locale), timezone, strict);
180     }
181
182     /**
183      * Turn a string into a Moment using the provided format string.
184      */
185     private momentize(date: string, format: string, timezone: string, strict: boolean): moment.Moment {
186         if (format.length) {
187             const result = moment.tz(date, format, true, timezone);
188             if (!result.isValid()) {
189                 if (strict) {
190                     throw new Error('Error parsing date ' + date);
191                 }
192                 return moment.tz(date, format, false, timezone);
193             }
194         return moment(new Date(date), timezone);
195         }
196     }
197
198     /**
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/)
202      *
203      * Returns a blank string if it can't do this transformation.
204      */
205     private makeFormatParseable(original: string, locale?: string): string {
206         if (!original) { return ''; }
207         if (!locale) { locale = this.locale.currentLocaleCode(); }
208         switch (original) {
209             case 'short': {
210                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Short);
211                 const date = getLocaleDateFormat(locale, FormatWidth.Short);
212                 const time = getLocaleTimeFormat(locale, FormatWidth.Short);
213                 original = template
214                     .replace('{1}', date)
215                     .replace('{0}', time)
216                     .replace(/\'(\w+)\'/, '[$1]');
217                 break;
218             }
219             case 'medium': {
220                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Medium);
221                 const date = getLocaleDateFormat(locale, FormatWidth.Medium);
222                 const time = getLocaleTimeFormat(locale, FormatWidth.Medium);
223                 original = template
224                     .replace('{1}', date)
225                     .replace('{0}', time)
226                     .replace(/\'(\w+)\'/, '[$1]');
227                 break;
228             }
229             case 'long': {
230                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Long);
231                 const date = getLocaleDateFormat(locale, FormatWidth.Long);
232                 const time = getLocaleTimeFormat(locale, FormatWidth.Long);
233                 original = template
234                     .replace('{1}', date)
235                     .replace('{0}', time)
236                     .replace(/\'(\w+)\'/, '[$1]');
237                 break;
238             }
239             case 'full': {
240                 const template = getLocaleDateTimeFormat(locale, FormatWidth.Full);
241                 const date = getLocaleDateFormat(locale, FormatWidth.Full);
242                 const time = getLocaleTimeFormat(locale, FormatWidth.Full);
243                 original = template
244                     .replace('{1}', date)
245                     .replace('{0}', time)
246                     .replace(/\'(\w+)\'/, '[$1]');
247                 break;
248             }
249             case 'shortDate': {
250                 original = getLocaleDateFormat(locale, FormatWidth.Short);
251                 break;
252             }
253             case 'mediumDate': {
254                 original = getLocaleDateFormat(locale, FormatWidth.Medium);
255                 break;
256             }
257             case 'longDate': {
258                 original = getLocaleDateFormat(locale, FormatWidth.Long);
259                 break;
260             }
261             case 'fullDate': {
262                 original = getLocaleDateFormat(locale, FormatWidth.Full);
263                 break;
264             }
265             case 'shortTime': {
266                 original = getLocaleTimeFormat(locale, FormatWidth.Short);
267                 break;
268             }
269             case 'mediumTime': {
270                 original = getLocaleTimeFormat(locale, FormatWidth.Medium);
271                 break;
272             }
273             case 'longTime': {
274                 original = getLocaleTimeFormat(locale, FormatWidth.Long);
275                 break;
276             }
277             case 'fullTime': {
278                 original = getLocaleTimeFormat(locale, FormatWidth.Full);
279                 break;
280             }
281         }
282         return original
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
294     }
295 }
296
297
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});
305     }
306 }
307