]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/format.service.ts
20f0fdff18a8c56eff21a1a1b018efdfce74019b
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / core / format.service.ts
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';
6
7 /**
8  * Format IDL vield values for display.
9  */
10
11 declare var OpenSRF;
12
13 export interface FormatParams {
14     value: any;
15     idlClass?: string;
16     idlField?: string;
17     datatype?: string;
18     orgField?: string; // 'shortname' || 'name'
19     datePlusTime?: boolean;
20     timezoneContextOrg?: number;
21 }
22
23 @Injectable({providedIn: 'root'})
24 export class FormatService {
25
26     dateFormat = 'shortDate';
27     dateTimeFormat = 'short';
28     wsOrgTimezone: string = OpenSRF.tz;
29
30     constructor(
31         private datePipe: DatePipe,
32         private currencyPipe: CurrencyPipe,
33         private idl: IdlService,
34         private org: OrgService
35     ) {
36
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
40         if (!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;
45             };
46         }
47     }
48
49     /**
50      * Create a human-friendly display version of any field type.
51      */
52     transform(params: FormatParams): string {
53         const value = params.value;
54
55         if (   value === undefined
56             || value === null
57             || value === ''
58             || Number.isNaN(value)) {
59             return '';
60         }
61
62         let datatype = params.datatype;
63
64         if (!datatype) {
65             if (params.idlClass && params.idlField) {
66                 datatype = this.idl.classes[params.idlClass]
67                     .field_map[params.idlField].datatype;
68             } else {
69                 // Assume it's a primitive value
70                 return value + '';
71             }
72         }
73
74         switch (datatype) {
75
76             case 'link':
77                 if (typeof value !== 'object') {
78                     return value + ''; // no fleshed value here
79                 }
80
81                 if (!params.idlClass || !params.idlField) {
82                     // Without a full accounting of the field data,
83                     // we can't determine the linked selector field.
84                     return value + '';
85                 }
86
87                 const selector =
88                     this.idl.getLinkSelector(params.idlClass, params.idlField);
89
90                 if (selector && typeof value[selector] === 'function') {
91                     const val = value[selector]();
92
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.
97                         return '';
98                     } else {
99                         return val + '';
100                     }
101
102                 } else {
103                     return value + '';
104                 }
105
106             case 'org_unit':
107                 const orgField = params.orgField || 'shortname';
108                 const org = this.org.get(value);
109                 return org ? org[orgField]() : '';
110
111             case 'timestamp':
112                 let tz;
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
117                     // local one
118                     tz = 'UTC';
119                 } else {
120                     tz = this.wsOrgTimezone;
121                 }
122                 const date = Moment(value).tz(tz);
123                 if (!date.isValid()) {
124                     console.error('Invalid date in format service', value);
125                     return '';
126                 }
127                 let fmt = this.dateFormat || 'shortDate';
128                 if (params.datePlusTime) {
129                     fmt = this.dateTimeFormat || 'short';
130                 }
131                 return this.datePipe.transform(date.toISOString(true), fmt, date.format('ZZ'));
132
133             case 'money':
134                 return this.currencyPipe.transform(value);
135
136             case 'bool':
137                 // Slightly better than a bare 't' or 'f'.
138                 // Note the caller is better off using an <eg-bool/> for
139                 // boolean display.
140                 return Boolean(
141                     value === 't' || value === 1 ||
142                     value === '1' || value === true
143                 ).toString();
144
145             default:
146                 return value + '';
147         }
148     }
149     /**
150      * Create an IDL-friendly display version of a human-readable date
151      */
152     idlFormatDate(date: string, timezone: string): string { return this.momentizeDateString(date, timezone).format('YYYY-MM-DD'); }
153
154     /**
155      * Create an IDL-friendly display version of a human-readable datetime
156      */
157     idlFormatDatetime(datetime: string, timezone: string): string { return this.momentizeDateTimeString(datetime, timezone).toISOString(); }
158
159     /**
160      * Turn a date string into a Moment using the date format org setting.
161      */
162     momentizeDateString(date: string, timezone: string, strict = false): Moment {
163         return this.momentize(date, this.makeFormatParseable(this.dateFormat), timezone, strict);
164     }
165
166     /**
167      * Turn a datetime string into a Moment using the datetime format org setting.
168      */
169     momentizeDateTimeString(date: string, timezone: string, strict = false): Moment {
170         return this.momentize(date, this.makeFormatParseable(this.dateTimeFormat), timezone, strict);
171     }
172
173     /**
174      * Turn a string into a Moment using the provided format string.
175      */
176     private momentize(date: string, format: string, timezone: string, strict: boolean): Moment {
177         if (format.length) {
178             const result = Moment.tz(date, format, true, timezone);
179             if (isNaN(result) || 'Invalid date' === result) {
180                 if (strict) {
181                     throw new Error('Error parsing date ' + date);
182                 }
183                 return Moment.tz(date, format, false, timezone);
184             }
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);
188         }
189     }
190
191     /**
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/)
195      *
196      * Returns a blank string if it can't do this transformation.
197      */
198     private makeFormatParseable(original: string): string {
199         if (!original) { return ''; }
200         switch (original) {
201             case 'short': {
202                 return 'M/D/YY, h:mm a';
203             }
204             case 'medium': {
205                 return 'MMM D, Y, h:mm:ss a';
206             }
207             case 'long': {
208                 return 'MMMM D, Y, h:mm:ss a [GMT]Z';
209             }
210             case 'full': {
211                 return 'dddd, MMMM D, Y, h:mm:ss a [GMT]Z';
212             }
213             case 'shortDate': {
214                 return 'M/D/YY';
215             }
216             case 'mediumDate': {
217                 return 'MMM D, Y';
218             }
219             case 'longDate': {
220                 return 'MMMM D, Y';
221             }
222             case 'fullDate': {
223                 return 'dddd, MMMM D, Y';
224             }
225             case 'shortTime': {
226                 return 'h:mm a';
227             }
228             case 'mediumTime': {
229                 return 'h:mm:ss a';
230             }
231             case 'longTime': {
232                 return 'h:mm:ss a [GMT]Z';
233             }
234             case 'fullTime': {
235                 return 'h:mm:ss a [GMT]Z';
236             }
237         }
238         return original
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
250     }
251 }
252
253
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});
261     }
262 }
263