Skip to content

Commit

Permalink
Date: Dynamically augment skeleton (3/3)
Browse files Browse the repository at this point in the history
Ref #271
Closes #604
Closes #462
  • Loading branch information
rxaviers committed Mar 17, 2017
1 parent c91e911 commit 687207b
Show file tree
Hide file tree
Showing 22 changed files with 608 additions and 234 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ before_install:
install:
- npm install
- bower install
sudo: false
2 changes: 1 addition & 1 deletion doc/api/date/date-formatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ For comparison, follow the same formatter `{ datetime: "short" }` on different l
| *pt* | `"01/11/10 17:55"` |
| *ar* | `"١‏/١١‏/٢٠١٠ ٥،٥٥ م"` |

Use skeletons for more flexibility (see its description [above](#parameters)).
Use open-ended skeletons for more flexibility (see its description [above](#parameters)). See some examples below.

| `skeleton` | `Globalize( "en" ).dateFormatter( skeleton )( new Date( 2010, 10, 1, 17, 55 ) )` |
| --- | --- |
Expand Down
1 change: 1 addition & 0 deletions src/build/intro-date.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var createError = Globalize._createError,
removeLiteralQuotes = Globalize._removeLiteralQuotes,
runtimeBind = Globalize._runtimeBind,
stringPad = Globalize._stringPad,
validate = Globalize._validate,
validateCldr = Globalize._validateCldr,
validateDefaultLocale = Globalize._validateDefaultLocale,
validateParameterPresence = Globalize._validateParameterPresence,
Expand Down
46 changes: 38 additions & 8 deletions src/date.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
define([
"cldr",
"./common/runtime-bind",
"./common/validate",
"./common/validate/cldr",
"./common/validate/default-locale",
"./common/validate/parameter-presence",
Expand All @@ -18,10 +19,10 @@ define([
"cldr/event",
"cldr/supplemental",
"./number"
], function( Cldr, runtimeBind, validateCldr, validateDefaultLocale, validateParameterPresence,
validateParameterTypeDate, validateParameterTypePlainObject, validateParameterTypeString,
Globalize, dateExpandPattern, dateFormatterFn, dateFormatProperties, dateParserFn,
dateParseProperties, dateTokenizerProperties ) {
], function( Cldr, runtimeBind, validate, validateCldr, validateDefaultLocale,
validateParameterPresence, validateParameterTypeDate, validateParameterTypePlainObject,
validateParameterTypeString, Globalize, dateExpandPattern, dateFormatterFn,
dateFormatProperties, dateParserFn, dateParseProperties, dateTokenizerProperties ) {

function validateRequiredCldr( path, value ) {
validateCldr( path, value, {
Expand All @@ -34,6 +35,31 @@ function validateRequiredCldr( path, value ) {
});
}

function validateOptionsPreset( options ) {
validateOptionsPresetEach( "date", options );
validateOptionsPresetEach( "time", options );
validateOptionsPresetEach( "datetime", options );
}

function validateOptionsPresetEach( type, options ) {
var value = options[ type ];
validate(
"E_INVALID_OPTIONS",
"Invalid `{{type}: \"{value}\"}`.",
value === undefined || [ "short", "medium", "long", "full" ].indexOf( value ) !== -1,
{ type: type, value: value }
);
}

function validateOptionsSkeleton( pattern, skeleton ) {
validate(
"E_INVALID_OPTIONS",
"Invalid `{skeleton: \"{value}\"}` based on provided CLDR.",
skeleton === undefined || ( typeof pattern === "string" && pattern ),
{ type: "skeleton", value: skeleton }
);
}

/**
* .dateFormatter( options )
*
Expand All @@ -58,12 +84,14 @@ Globalize.prototype.dateFormatter = function( options ) {
cldr = this.cldr;
options = options || { skeleton: "yMd" };

args = [ options ];

validateOptionsPreset( options );
validateDefaultLocale( cldr );

args = [ options ];

cldr.on( "get", validateRequiredCldr );
pattern = dateExpandPattern( options, cldr );
validateOptionsSkeleton( pattern, options.skeleton );
properties = dateFormatProperties( pattern, cldr );
cldr.off( "get", validateRequiredCldr );

Expand Down Expand Up @@ -100,12 +128,14 @@ Globalize.prototype.dateParser = function( options ) {
cldr = this.cldr;
options = options || { skeleton: "yMd" };

args = [ options ];

validateOptionsPreset( options );
validateDefaultLocale( cldr );

args = [ options ];

cldr.on( "get", validateRequiredCldr );
pattern = dateExpandPattern( options, cldr );
validateOptionsSkeleton( pattern, options.skeleton );
tokenizerProperties = dateTokenizerProperties( pattern, cldr );
parseProperties = dateParseProperties( cldr );
cldr.off( "get", validateRequiredCldr );
Expand Down
205 changes: 45 additions & 160 deletions src/date/expand-pattern.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
define([
"../common/format-message",
"../common/create-error/invalid-parameter-value",
"./pattern-re"
], function( formatMessage, createErrorInvalidParameterValue, datePatternRe ) {
"./expand-pattern/get-best-match-pattern"
], function( formatMessage, createErrorInvalidParameterValue,
dateExpandPatternGetBestMatchPattern ) {

/**
* expandPattern( options, cldr )
Expand All @@ -25,9 +26,11 @@ define([
* - { datetime: "full" } returns "EEEE, MMMM d, y 'at' h:mm:ss a zzzz";
* - { raw: "dd/mm" } returns "dd/mm";
*/

return function( options, cldr ) {
var dateSkeleton, result, skeleton, timeSkeleton, type, dateTimeSkeleton;
var dateSkeleton, result, skeleton, timeSkeleton, type,

// Using easier to read variables.
getBestMatchPattern = dateExpandPatternGetBestMatchPattern;

function combineDateTime( type, datePattern, timePattern ) {
return formatMessage(
Expand All @@ -39,171 +42,53 @@ return function( options, cldr ) {
);
}

function getBestMatchPattern( path, skeleton ) {
var availableFormats, ratedFormats, format, pattern;

pattern = cldr.main([ path, skeleton ]);

if ( skeleton && !pattern ) {
availableFormats = cldr.main([ path ]);
ratedFormats = [];

for ( format in availableFormats ) {
ratedFormats.push({
format: format,
pattern: availableFormats[format],
rate: compareFormats( skeleton, format )
});
}
switch ( true ) {
case "skeleton" in options:
skeleton = options.skeleton;

ratedFormats = ratedFormats
.filter( function( format ) {
return format.rate > -1;
} )
.sort( function( formatA, formatB ) {
return formatA.rate - formatB.rate;
});
// Preferred hour (j).
skeleton = skeleton.replace( /j/g, function() {
return cldr.supplemental.timeData.preferred();
});

if ( ratedFormats.length ) {
pattern = augmentFormat( skeleton, ratedFormats[0].pattern );
// Try direct map (note that getBestMatchPattern handles it).
// ... or, try to "best match" the whole skeleton.
result = getBestMatchPattern(
cldr,
skeleton
);
if ( result ) {
break;
}
}

return pattern;
}

function repeatStr( str, count ) {
var i, result = "";
for ( i = 0; i < count; i++ ) {
result = result + str;
}
return result;
}

function normalizePatternType( char ) {
switch ( char ) {
case "e":
case "E":
case "c":
return "e";

case "M":
case "L":
return "L";

default:
return char;
}
}
// ... or, try to "best match" the date and time parts individually.
timeSkeleton = skeleton.split( /[^hHKkmsSAzZOvVXx]/ ).slice( -1 )[ 0 ];
dateSkeleton = skeleton.split( /[^GyYuUrQqMLlwWdDFgEec]/ )[ 0 ];
dateSkeleton = getBestMatchPattern(
cldr,
dateSkeleton
);
timeSkeleton = getBestMatchPattern(
cldr,
timeSkeleton
);

function compareFormats( formatA, formatB ) {
var distance,
typeA,
typeB,
matchFound,
i,
j;

if ( formatA === formatB ) {
return 0;
}

formatA = formatA.match( datePatternRe );
formatB = formatB.match( datePatternRe );
if ( formatA.length === formatB.length ) {
distance = 1;
for ( i = 0; i < formatA.length; i++ ) {
typeA = normalizePatternType( formatA[i].charAt( 0 ) );
typeB = null;
matchFound = false;
for ( j = 0; j < formatB.length; j++ ) {
typeB = normalizePatternType( formatB[j].charAt( 0 ) );
if ( typeA === typeB ) {
break;
} else {
typeB = null;
}
}
if ( null === typeB ) {
return -1;
}
distance = distance + Math.abs( formatA[i].length - formatB[j].length );
if ( formatA[i].charAt( 0 ) !== formatB[j].charAt( 0 ) ) {
distance = distance + 1;
}
if ( /(MMMM|LLLL).*[Ec]/.test( dateSkeleton ) ) {
type = "full";
} else if ( /MMMM|LLLL/.test( dateSkeleton ) ) {
type = "long";
} else if ( /MMM|LLL/.test( dateSkeleton ) ) {
type = "medium";
} else {
type = "short";
}
return distance;
}
return -1;
}

function augmentFormat( requestedSkeleton, bestMatchFormat ) {
var i, j, matchedType, matchedLength, requestedType, requestedLength;

requestedSkeleton = requestedSkeleton.match( datePatternRe );
bestMatchFormat = bestMatchFormat.match( datePatternRe );

for ( i = 0; i < bestMatchFormat.length; i++ ) {
matchedType = bestMatchFormat[i].charAt( 0 );
matchedLength = bestMatchFormat[i].length;
for ( j = 0; j < requestedSkeleton.length; j++ ) {
requestedType = requestedSkeleton[j].charAt( 0 );
requestedLength = requestedSkeleton[j].length;
if (
normalizePatternType( matchedType ) === normalizePatternType( requestedType ) &&
matchedLength < requestedLength
) {
bestMatchFormat[i] = repeatStr( matchedType, requestedLength );
}
if ( dateSkeleton && timeSkeleton ) {
result = combineDateTime( type, dateSkeleton, timeSkeleton );
} else {
result = dateSkeleton || timeSkeleton;
}
}

return bestMatchFormat.join( "" );
}

switch ( true ) {
case "skeleton" in options:
skeleton = options.skeleton;
result = cldr.main([
"dates/calendars/gregorian/dateTimeFormats/availableFormats",
skeleton
]);
if ( !result ) {
timeSkeleton = skeleton.split( /[^hHKkmsSAzZOvVXx]/ ).slice( -1 )[ 0 ];
dateSkeleton = skeleton.split( /[^GyYuUrQqMLlwWdDFgEec]/ )[ 0 ];
dateTimeSkeleton = getBestMatchPattern(
"dates/calendars/gregorian/dateTimeFormats/availableFormats",
skeleton
);
if ( dateTimeSkeleton ) {
result = dateTimeSkeleton;
} else {
dateSkeleton = getBestMatchPattern(
"dates/calendars/gregorian/dateTimeFormats/availableFormats",
dateSkeleton
);
timeSkeleton = getBestMatchPattern(
"dates/calendars/gregorian/dateTimeFormats/availableFormats",
timeSkeleton
);

if ( /(MMMM|LLLL).*[Ec]/.test( dateSkeleton ) ) {
type = "full";
} else if ( /MMMM/g.test( dateSkeleton ) ) {
type = "long";
} else if ( /MMM/g.test( dateSkeleton ) || /LLL/g.test( dateSkeleton ) ) {
type = "medium";
} else {
type = "short";
}

if ( dateSkeleton && timeSkeleton ) {
result = combineDateTime( type, dateSkeleton, timeSkeleton );
} else {
result = dateSkeleton || timeSkeleton;
}
}
}
break;

case "date" in options:
Expand Down
33 changes: 33 additions & 0 deletions src/date/expand-pattern/augment-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
define([
"./normalize-pattern-type",
"../pattern-re",
"../../util/string/repeat"
], function( dateExpandPatternNormalizePatternType, datePatternRe, stringRepeat ) {

return function( requestedSkeleton, bestMatchFormat ) {
var i, j, matchedType, matchedLength, requestedType, requestedLength,

// Using an easier to read variable.
normalizePatternType = dateExpandPatternNormalizePatternType;

requestedSkeleton = requestedSkeleton.match( datePatternRe );
bestMatchFormat = bestMatchFormat.match( datePatternRe );

for ( i = 0; i < bestMatchFormat.length; i++ ) {
matchedType = bestMatchFormat[i].charAt( 0 );
matchedLength = bestMatchFormat[i].length;
for ( j = 0; j < requestedSkeleton.length; j++ ) {
requestedType = requestedSkeleton[j].charAt( 0 );
requestedLength = requestedSkeleton[j].length;
if ( normalizePatternType( matchedType ) === normalizePatternType( requestedType ) &&
matchedLength < requestedLength
) {
bestMatchFormat[i] = stringRepeat( matchedType, requestedLength );
}
}
}

return bestMatchFormat.join( "" );
};

});
Loading

0 comments on commit 687207b

Please sign in to comment.