diff --git a/src/common/validate/skeleton.js b/src/common/validate/skeleton.js new file mode 100644 index 000000000..24c43adaa --- /dev/null +++ b/src/common/validate/skeleton.js @@ -0,0 +1,48 @@ +define([ + "../create-error", + "./skeleton/fields-pos-map" +], function( createError, validateSkeletonFieldsPosMap ) { + +/** + * validateSkeleton( skeleton ) + * + * skeleton: Assume `j` has already been converted into a localized hour field. + */ +return function validateSkeleton( skeleton ) { + var last, + + // Using easier to read variable. + fieldsPosMap = validateSkeletonFieldsPosMap; + + // "The fields are from the Date Field Symbol Table in Date Format Patterns" + // Ref: http://www.unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems + // I.e., check for invalid characters. + skeleton.replace( /[^GyYuUrQqMLlwWEecdDFghHKkmsSAzZOvVXx]/, function( field ) { + throw createError( + "E_INVALID_OPTIONS", "Invalid field `{field}` of skeleton `{skeleton}`", + { + field: field, + skeleton: skeleton + } + ); + }); + + // "The canonical order is from top to bottom in that table; that is, yM not My". + // http://www.unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems + // I.e., check for invalid order. + skeleton.split( "" ).every(function( field ) { + if ( fieldsPosMap[ field ] < last ) { + throw createError( + "E_INVALID_OPTIONS", "Invalid order `{field}` of skeleton `{skeleton}`", + { + field: field, + skeleton: skeleton + } + ); + } + last = fieldsPosMap[ field ]; + return true; + }); +}; + +}); diff --git a/src/common/validate/skeleton/fields-pos-map.js b/src/common/validate/skeleton/fields-pos-map.js new file mode 100644 index 000000000..65f380530 --- /dev/null +++ b/src/common/validate/skeleton/fields-pos-map.js @@ -0,0 +1,16 @@ +define(function() { + +/** + * Create a map between the skeleton fields and their positions, e.g., + * { + * G: 0 + * y: 1 + * ... + * } + */ +return "GyYuUrQqMLlwWEecdDFghHKkmsSAzZOvVXx".split( "" ).reduce(function( memo, item, i ) { + memo[ item ] = i; + return memo; +}, {}); + +}); diff --git a/src/date/expand-pattern.js b/src/date/expand-pattern.js index ce9729cd5..34eefb3cf 100644 --- a/src/date/expand-pattern.js +++ b/src/date/expand-pattern.js @@ -1,8 +1,9 @@ define([ - "../common/format-message", "../common/create-error/invalid-parameter-value", + "../common/format-message", + "../common/validate/skeleton", "./expand-pattern/get-best-match-pattern" -], function( formatMessage, createErrorInvalidParameterValue, +], function( createErrorInvalidParameterValue, formatMessage, validateSkeleton, dateExpandPatternGetBestMatchPattern ) { /** @@ -51,6 +52,8 @@ return function( options, cldr ) { return cldr.supplemental.timeData.preferred(); }); + validateSkeleton( skeleton ); + // Try direct map (note that getBestMatchPattern handles it). // ... or, try to "best match" the whole skeleton. result = getBestMatchPattern( diff --git a/test/unit/date/expand-pattern.js b/test/unit/date/expand-pattern.js index f5c58a8a0..935200474 100644 --- a/test/unit/date/expand-pattern.js +++ b/test/unit/date/expand-pattern.js @@ -43,36 +43,36 @@ QUnit.test( "should expand {skeleton: \"\"}", function( assert ) { assert.expandPattern( de, { skeleton: "jmm" }, "HH:mm" ); assert.expandPattern( ru, { skeleton: "jmm" }, "H:mm" ); - // Best match the whole skeleton. + // Best matches the whole skeleton. assert.expandPattern( en, { skeleton: "hhmm" }, "hh:mm a" ); assert.expandPattern( en, { skeleton: "HHmm" }, "HH:mm" ); assert.expandPattern( en, { skeleton: "EHmss" }, "E HH:mm:ss" ); assert.expandPattern( en, { skeleton: "yy" }, "yy" ); assert.expandPattern( de, { skeleton: "yMMMMd" }, "d. MMMM y" ); assert.expandPattern( de, { skeleton: "MMMM" }, "LLLL" ); - assert.expandPattern( de, { skeleton: "MMMMy" }, "MMMM y" ); + assert.expandPattern( de, { skeleton: "yMMMM" }, "MMMM y" ); assert.expandPattern( de, { skeleton: "EEEE" }, "cccc" ); assert.expandPattern( de, { skeleton: "cccc" }, "cccc" ); - assert.expandPattern( de, { skeleton: "EEEEMMMMd" }, "EEEE, d. MMMM" ); - assert.expandPattern( de, { skeleton: "ccccMMMMd" }, "EEEE, d. MMMM" ); + assert.expandPattern( de, { skeleton: "MMMMEEEEd" }, "EEEE, d. MMMM" ); + assert.expandPattern( de, { skeleton: "MMMMccccd" }, "EEEE, d. MMMM" ); assert.expandPattern( de, { skeleton: "HHmm" }, "HH:mm" ); assert.expandPattern( de, { skeleton: "EEEEHHmm" }, "EEEE, HH:mm" ); assert.expandPattern( de, { skeleton: "EEEEHmm" }, "EEEE, HH:mm" ); assert.expandPattern( de, { skeleton: "ccccHmm" }, "EEEE, HH:mm" ); assert.expandPattern( ru, { skeleton: "yMMMMd" }, "d MMMM y 'г'." ); assert.expandPattern( ru, { skeleton: "MMMM" }, "LLLL" ); - assert.expandPattern( ru, { skeleton: "MMMMy" }, "LLLL y 'г'." ); + assert.expandPattern( ru, { skeleton: "yMMMM" }, "LLLL y 'г'." ); assert.expandPattern( ru, { skeleton: "EEEE" }, "cccc" ); assert.expandPattern( ru, { skeleton: "cccc" }, "cccc" ); - assert.expandPattern( ru, { skeleton: "EEEEMMMMd" }, "cccc, d MMMM" ); - assert.expandPattern( ru, { skeleton: "ccccMMMMd" }, "cccc, d MMMM" ); + assert.expandPattern( ru, { skeleton: "MMMMEEEEd" }, "cccc, d MMMM" ); + assert.expandPattern( ru, { skeleton: "MMMMccccd" }, "cccc, d MMMM" ); assert.expandPattern( ru, { skeleton: "HHmm" }, "HH:mm" ); assert.expandPattern( ru, { skeleton: "EEEEHHmm" }, "EEEE HH:mm" ); assert.expandPattern( ru, { skeleton: "EEEEHmm" }, "EEEE HH:mm" ); assert.expandPattern( ru, { skeleton: "ccccHHmm" }, "EEEE HH:mm" ); assert.expandPattern( ru, { skeleton: "ccccHmm" }, "EEEE HH:mm" ); - // Best match the date and time parts individually then combine together. + // Best matches the date and time parts individually then combine them together. assert.expandPattern( en, { skeleton: "GyMMMEdhms" }, "E, MMM d, y G, h:mm:ss a" ); assert.expandPattern( en, { skeleton: "MMMMEdhm" }, "E, MMMM d 'at' h:mm a" ); assert.expandPattern( en, { skeleton: "MMMMh" }, "LLLL 'at' h a" ); @@ -80,6 +80,25 @@ QUnit.test( "should expand {skeleton: \"\"}", function( assert ) { assert.expandPattern( ru, { skeleton: "MMMMEdhm" }, "ccc, d MMMM, h:mm a" ); }); +QUnit.test( "should throw exception on invalid skeletons", function( assert ) { + // Invalid characters. + assert.throws(function() { + expandPattern({ skeleton: "MMM d" }, en ); + }); + assert.throws(function() { + expandPattern({ skeleton: "MM/dd" }, en ); + }); + + // Invalid order. + assert.throws(function() { + expandPattern({ skeleton: "dM" }, en ); + }); + assert.throws(function() { + expandPattern({ skeleton: "My" }, en ); + expandPattern({ skeleton: "MMMy" }, en ); + }); +}); + QUnit.test( "should expand {date: \"(full, ...)\"}", function( assert ) { assert.expandPattern( en, { date: "full" }, "EEEE, MMMM d, y" ); });