Repository: moment/luxon (js)

Issue:
Library fails to parse time using fromFormat
```
Settings.defaultLocale = 'es-ES';

DateTime.fromFormat('11:22 am', 'hh:mm a');
```
returns `"the input "11:22 am" can't be parsed as format hh:mm a"`
in case of `es-ES` locale `/^(\d{2})(:)(\d{2})(\ )(a\.? m.|p\.? m.)$/i` was used and for `en-US` - `/^(\d{2})(:)(\d{2})(\ )(AM|PM)$/i`

expected result: `DateTime.fromFormat('11:22 am', 'hh:mm a')` to be parsed without exceptions

Pull request:
fromFormat handles non breakable spaces. Fixes #714
The problem was the the `es-ES` meridiem strings are _"a. m."_ and _"p. m."_, but with a **non breakable space** in the middle (char code 160). Since the input string used a regular space, it was not matched correctly.

This PR changes the RegExp to consider spaces and non breakable spaces characters interchangeably.

It also adds a `global` flag to the dots, so that they are *all* considered optional.

Patch:
diff --git a/src/impl/tokenParser.js b/src/impl/tokenParser.js
index 88560ae2..4ca0401c 100644
--- a/src/impl/tokenParser.js
+++ b/src/impl/tokenParser.js
@@ -12,13 +12,21 @@ function intUnit(regex, post = i => i) {
   return { regex, deser: ([s]) => post(parseDigits(s)) };
 }
 
+const NBSP = String.fromCharCode(160);
+const spaceOrNBSP = `( |${NBSP})`;
+const spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, "g");
+
 function fixListRegex(s) {
   // make dots optional and also make them literal
-  return s.replace(/\./, "\\.?");
+  // make space and non breakable space characters interchangeable
+  return s.replace(/\./g, "\\.?").replace(spaceOrNBSPRegExp, spaceOrNBSP);
 }
 
 function stripInsensitivities(s) {
-  return s.replace(/\./, "").toLowerCase();
+  return s
+    .replace(/\./g, "") // ignore dots that were made optional
+    .replace(spaceOrNBSPRegExp, " ") // interchange space and nbsp
+    .toLowerCase();
 }
 
 function oneOf(strings, startIndex) {


Test patch:
diff --git a/test/datetime/create.test.js b/test/datetime/create.test.js
index fccba891..742f24c9 100644
--- a/test/datetime/create.test.js
+++ b/test/datetime/create.test.js
@@ -3,7 +3,7 @@
 import { DateTime } from "../../src/luxon";
 import Helpers from "../helpers";
 
-const withDefaultLocale = Helpers.setUnset("defaultLocale"),
+const withDefaultLocale = Helpers.withDefaultLocale,
   withDefaultNumberingSystem = Helpers.setUnset("defaultNumberingSystem"),
   withDefaultOutputCalendar = Helpers.setUnset("defaultOutputCalendar"),
   withthrowOnInvalid = Helpers.setUnset("throwOnInvalid");
diff --git a/test/datetime/tokenParse.test.js b/test/datetime/tokenParse.test.js
index 984bb4b0..e1dbff76 100644
--- a/test/datetime/tokenParse.test.js
+++ b/test/datetime/tokenParse.test.js
@@ -1,5 +1,7 @@
 /* global test expect */
 import { DateTime } from "../../src/luxon";
+import Helpers from "../helpers";
+
 import { ConflictingSpecificationError } from "../../src/errors";
 
 //------
@@ -16,7 +18,7 @@ test("DateTime.fromFormat() parses basic times", () => {
   expect(i.millisecond).toBe(445);
 });
 
-test("DateTime.fromFormat() parses with variable-length inpus", () => {
+test("DateTime.fromFormat() parses with variable-length input", () => {
   let i = DateTime.fromFormat("1982/05/03 09:07:05.004", "y/M/d H:m:s.S");
   expect(i.year).toBe(1982);
   expect(i.month).toBe(5);
@@ -66,6 +68,41 @@ test("DateTime.fromFormat() throws if you specify meridiem with 24-hour time", (
   expect(() => DateTime.fromFormat("930PM", "Hmma")).toThrow(ConflictingSpecificationError);
 });
 
+// #714
+test("DateTime.fromFormat() makes dots optional and handles non breakable spaces", () => {
+  function parseMeridiem(input, isAM) {
+    const d = DateTime.fromFormat(input, "hh:mm a");
+    expect(d.hour).toBe(isAM ? 10 : 22);
+    expect(d.minute).toBe(45);
+    expect(d.second).toBe(0);
+  }
+
+  // Meridiem for this locale is "a. m." or "p. m.", with a non breakable space
+  Helpers.withDefaultLocale("es-ES", () => {
+    parseMeridiem("10:45 a. m.", true);
+    parseMeridiem("10:45 a. m", true);
+    parseMeridiem("10:45 a m.", true);
+    parseMeridiem("10:45 a m", true);
+
+    parseMeridiem("10:45 p. m.", false);
+    parseMeridiem("10:45 p. m", false);
+    parseMeridiem("10:45 p m.", false);
+    parseMeridiem("10:45 p m", false);
+
+    const nbsp = String.fromCharCode(160);
+
+    parseMeridiem(`10:45 a.${nbsp}m.`, true);
+    parseMeridiem(`10:45 a.${nbsp}m`, true);
+    parseMeridiem(`10:45 a${nbsp}m.`, true);
+    parseMeridiem(`10:45 a${nbsp}m`, true);
+
+    parseMeridiem(`10:45 p.${nbsp}m.`, false);
+    parseMeridiem(`10:45 p.${nbsp}m`, false);
+    parseMeridiem(`10:45 p${nbsp}m.`, false);
+    parseMeridiem(`10:45 p${nbsp}m`, false);
+  });
+});
+
 test("DateTime.fromFormat() parses variable-digit years", () => {
   expect(DateTime.fromFormat("", "y").isValid).toBe(false);
   expect(DateTime.fromFormat("2", "y").year).toBe(2);