LP1840782 locale/timezone aware date display formatter
authorBill Erickson <berickxx@gmail.com>
Wed, 21 Aug 2019 18:06:03 +0000 (14:06 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 21 Aug 2019 19:26:18 +0000 (15:26 -0400)
Adds a method to the DateUtil class which can generate a human-friendly,
timezone and locale-aware display value for a date.  It supports canned
date and time formats, following the options described here (see
dateStyle and timeStyle):

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat

Adds a tool to the sandbox for generating date strings based on locale,
time zone, date format, and time format.

As noted in the sandbox page and within the utility code, Firefox
support for canned dateStyle and timeStyle values is pending:

resolution of https://bugzilla.mozilla.org/show_bug.cgi?id=1557718.

In the meantime, the code generates locale-aware "short" date and time
values in Firefox.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/util/date.spec.ts
Open-ILS/src/eg2/src/app/share/util/date.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts

index 2ef5a42..6b048a2 100644 (file)
@@ -35,4 +35,15 @@ describe('DateUtil', () => {
     it('Cross walk YMD and ISO', () => {
         expect(DateUtil.isoToLocalYmd(DateUtil.ymdToLocalIso(ymd))).toBe(ymd);
     });
+
+    /* Intl API not yet supported on PhantomJs
+    it('Creates an cs-CZ short date display string', () => {
+        const formatted = DateUtil.dateToLocaleString(now,
+            {locale: 'cs-CZ', dateStyle: 'short', timeStyle: 'short'});
+
+        // cs-CZ short date/time example 21.08.19 20:17
+        expect(formatted.match(/^\d\d\.\d\d\.\d\d \d\d:\d\d$/)).not.toBe(null);
+    });
+    */
+
 });
index 6f2516f..19f90f8 100644 (file)
@@ -1,5 +1,19 @@
 /* Collection of date utility functions */
 
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+interface DateFormatOptions {
+    locale?: string; // defaults to browser locale
+    timeZone?: string; // defaults to browser time zone
+
+    // Note dateStyle and timeStyle support a value of 'default' in addition
+    // to the documented options.
+    dateStyle?: string;
+    timeStyle?: string;
+}
+
+const DEFAULT_DATE_STYLE = 'short'; // en-US => 8/21/19
+const DEFAULT_TIME_STYLE = 'short'; // en-US => 12:01 PM
+
 export class DateUtil {
 
     // Returns a YYYY-MM-DD string in the local time zone matching
@@ -39,4 +53,61 @@ export class DateUtil {
         if (!d) { return null; }
         return d.toISOString();
     }
+
+
+    // Returns a locale-friendly display string for a date.
+    // To include the date in the output, set a value for options.dateStyle
+    // To include the time in the output, set a value for options.timeStyle
+    // If dateStyle or timeStyle have the value 'default', the application
+    // default format will be used.
+    // If both dateStyle and timeStyle are null, the fallback mode is
+    // dateStyle === 'default'
+    // For browsers where dateStyle and timeStyle are not supported,
+    // 'short' date / time styles are always used.  (see inline comments).
+    static dateToLocaleString(
+        date: Date, options: DateFormatOptions): string {
+
+        if (options.dateStyle === 'default') {
+            options.dateStyle = DEFAULT_DATE_STYLE;
+        }
+
+        if (options.timeStyle === 'default') {
+            options.timeStyle = DEFAULT_DATE_STYLE;
+        }
+
+        if (!options.dateStyle && !options.timeStyle) {
+            options.dateStyle = DEFAULT_DATE_STYLE;
+        }
+
+        const locale = options.locale || null; // defaults to browser locale
+        delete options.locale;
+
+        let formatter = new Intl.DateTimeFormat(locale, options);
+
+        // Firefox support for dateStyle / timeStyle pending.
+        // https://bugzilla.mozilla.org/show_bug.cgi?id=1557718
+        // For now, use numeric date / style formats, which are
+        // equivalent to 'short' dates and times (at least for the 9
+        // locales provided w/ Evergreen by default -- presumably this
+        // is universal).  TODO: remove this code once the feature is
+        // generally available in FF.
+        const resolved: any = formatter.resolvedOptions();
+
+        if (!resolved.dateStyle && !resolved.timeStyle) { // supported?
+
+            const optionsFf: any = {timeZone: options.timeZone};
+
+            if (options.dateStyle) {
+                optionsFf.year = optionsFf.month = optionsFf.day = 'numeric';
+            }
+
+            if (options.timeStyle) {
+                optionsFf.hour = optionsFf.minute = 'numeric';
+            }
+
+            formatter = new Intl.DateTimeFormat(locale, optionsFf);
+        }
+
+        return formatter.format(date);
+    }
 }
index adb01d3..1944656 100644 (file)
   </div>
 </div>
 
+<div class="w-100 border-top border-bottom border-primary p-2 mb-2 mt-2">
+  <h5>Date Display Generator -- Browser Time is {{dateNow}}</h5>
+  <div class="font-italic">
+    Note we only support 'short' date and time styles for Firefox, pending
+    resolution of <a href='https://bugzilla.mozilla.org/show_bug.cgi?id=1557718'>
+      https://bugzilla.mozilla.org/show_bug.cgi?id=1557718</a>.
+  </div>
+  <div class="row p-1 m-1 border">
+    <div class="col-lg-2">
+      <eg-combobox [entries]="localeCodes" placeholder="Date Locale" 
+        (onChange)="dateLocale = $event.id; setDateString()"> 
+      </eg-combobox>
+    </div>
+    <div class="col-lg-2">
+      <eg-combobox placeholder="Date Time Zone" (onChange)="dateTimeZone = $event.id; setDateString()">
+        <eg-combobox-entry entryId="America/New_York" entryLabel="America/New_York"></eg-combobox-entry>
+        <eg-combobox-entry entryId="America/Los_Angeles" entryLabel="America/Los_Angeles"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Europe/Prague" entryLabel="Europe/Prague"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Europe/Helsinki" entryLabel="Europe/Helsinki"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Europe/Paris" entryLabel="Europe/Paris"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Europe/Madrid" entryLabel="Europe/Madrid"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Europe/Kiev" entryLabel="Europe/Kiev"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Asia/Amman" entryLabel="Asia/Amman"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Canada/Eastern" entryLabel="Canada/Eastern"></eg-combobox-entry>
+        <eg-combobox-entry entryId="Canada/Mountain" entryLabel="Canada/Mountain"></eg-combobox-entry>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-2">
+      <eg-combobox placeholder="Date Style" (onChange)="dateStyle = $event.id; setDateString()">
+        <eg-combobox-entry entryId="" entryLabel="No Date"></eg-combobox-entry>
+        <eg-combobox-entry entryId="full" entryLabel="full"></eg-combobox-entry>
+        <eg-combobox-entry entryId="long" entryLabel="long"></eg-combobox-entry>
+        <eg-combobox-entry entryId="medium" entryLabel="medium"></eg-combobox-entry>
+        <eg-combobox-entry entryId="short" entryLabel="short"></eg-combobox-entry>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-2">
+      <eg-combobox placeholder="Time Style" (onChange)="timeStyle = $event.id; setDateString()">
+        <eg-combobox-entry entryId="" entryLabel="No Time"></eg-combobox-entry>
+        <eg-combobox-entry entryId="full" entryLabel="full"></eg-combobox-entry>
+        <eg-combobox-entry entryId="long" entryLabel="long"></eg-combobox-entry>
+        <eg-combobox-entry entryId="medium" entryLabel="medium"></eg-combobox-entry>
+        <eg-combobox-entry entryId="short" entryLabel="short"></eg-combobox-entry>
+      </eg-combobox>
+    </div>
+    <div class="col-lg-4 font-weight-bold text-primary">{{dateString}}</div>
+  </div>
+</div>
+
 <!-- printing -->
 
 <button class="btn btn-secondary" (click)="doPrint()">Test Print</button>
     </div>
   </div>
 </div>
+
index c452e15..3e9d7a7 100644 (file)
@@ -1,4 +1,3 @@
-
 import {timer as observableTimer, Observable, of} from 'rxjs';
 import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core';
 import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
@@ -19,6 +18,7 @@ import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {FormatService} from '@eg/core/format.service';
 import {StringComponent} from '@eg/share/string/string.component';
 import {GridComponent} from '@eg/share/grid/grid.component';
+import {DateUtil} from '@eg/share/util/date';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -45,6 +45,13 @@ export class SandboxComponent implements OnInit {
     @ViewChild('bresvEditor')
     private bresvEditor: FmRecordEditorComponent;
 
+    localeCodes: ComboboxEntry[] = [];
+    dateLocale: string;
+    dateTimeZone: string;
+    dateStyle: string;
+    timeStyle: string;
+    dateString: string;
+    dateNow = new Date();
 
     // @ViewChild('helloStr') private helloStr: StringComponent;
 
@@ -251,6 +258,13 @@ export class SandboxComponent implements OnInit {
         b.cancel_time('2019-03-25T11:07:59-0400');
         this.bresvEditor.mode = 'create';
         this.bresvEditor.record = b;
+
+        this.pcrud.retrieveAll('i18n_l').subscribe(locale => {
+            if (!this.dateLocale) {
+                this.dateLocale = locale.code();
+            }
+            this.localeCodes.push({id: locale.code(), label: locale.code()});
+        });
     }
 
     sbChannelHandler = msg => {
@@ -381,6 +395,17 @@ export class SandboxComponent implements OnInit {
             );
         });
     }
+
+    setDateString() {
+        const options: any = {};
+
+        if (this.dateLocale) { options.locale = this.dateLocale; }
+        if (this.dateTimeZone) { options.timeZone = this.dateTimeZone; }
+        if (this.dateStyle) { options.dateStyle = this.dateStyle; }
+        if (this.timeStyle) { options.timeStyle = this.timeStyle; }
+
+        this.dateString = DateUtil.dateToLocaleString(new Date(), options);
+    }
 }