Skip to content

Commit

Permalink
Time only values do not respect custom TIME format specifying fractio…
Browse files Browse the repository at this point in the history
…ns of a second Fixes #185
  • Loading branch information
cfsimplicity committed Mar 29, 2020
1 parent ffb305c commit aaf40ba
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 17 deletions.
51 changes: 35 additions & 16 deletions Spreadsheet.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,10 @@ component accessors="true"{
setCellValueAsType( arguments.workbook, cell, value, "numeric" );
break;
case "DATE":
setCellValueAsType( arguments.workbook, cell, value, "date" );
break;
case "TIME":
setCellValueAsType( arguments.workbook, cell, value, "date" );
setCellValueAsType( arguments.workbook, cell, value, "time" );
break;
case "BOOLEAN":
setCellValueAsType( arguments.workbook, cell, value, "boolean" );
Expand Down Expand Up @@ -2070,7 +2072,7 @@ component accessors="true"{
/* Get numeric cell data. This could be a standard number, could also be a date value. */
if( getDateUtil().isCellDateFormatted( arguments.cell ) ){
var cellValue = arguments.cell.getDateCellValue();
if( DateCompare( "1899-12-31", cellValue, "d" ) EQ 0 ) // TIME
if( isTimeOnlyValue( cellValue ) )
return getFormatter().formatCellValue( arguments.cell );//return as a time formatted string to avoid default epoch date 1899-12-31
return cellValue;
}
Expand Down Expand Up @@ -2103,17 +2105,26 @@ component accessors="true"{
return variables.dateUtil;
}

private string function getDateTimeValueFormat( required any value ){
private string function getDateTimeValueFormat( required date value ){
/* Returns the default date mask for the given value: DATE (only), TIME (only) or TIMESTAMP */
var dateTime = ParseDateTime( arguments.value );
var dateOnly = CreateDate( Year( dateTime ), Month( dateTime ), Day( dateTime ) );
if( DateCompare( arguments.value, dateOnly, "s" ) EQ 0 )
var dateOnly = CreateDate( Year( arguments.value ), Month( arguments.value ), Day( arguments.value ) );
if( isDateOnlyValue( arguments.value ) )
return this.getDateFormats().DATE;
if( DateCompare( "1899-12-30", dateOnly, "d" ) EQ 0 )
if( isTimeOnlyValue( arguments.value ) )
return this.getDateFormats().TIME;
return this.getDateFormats().TIMESTAMP;
}

private boolean function isDateOnlyValue( required date value ){
var dateOnly = CreateDate( Year( arguments.value ), Month( arguments.value ), Day( arguments.value ) );
return ( DateCompare( arguments.value, dateOnly, "s" ) == 0 );
}

private boolean function isTimeOnlyValue( required date value ){
//NB: this will only detect CF time object (epoch = 1899-12-30), not those using unix epoch 1970-01-01
return ( Year( arguments.value ) == "1899" );
}

private numeric function getDefaultCharWidth( required workbook ){
/* Estimates the default character width using Excel's 'Normal' font */
/* this is a compromise between hard coding a default value and the more complex method of using an AttributedString and TextLayout */
Expand Down Expand Up @@ -2475,7 +2486,7 @@ component accessors="true"{
private void function setCellValueAsType( required workbook, required cell, required value, string type ){
if( !arguments.KeyExists( "type" ) ) //autodetect type
arguments.type = detectValueDataType( arguments.value );
else if( !ListFindNoCase( "string,numeric,date,boolean,blank", arguments.type ) )
else if( !ListFindNoCase( "string,numeric,date,time,boolean,blank", arguments.type ) )
Throw( type=this.getExceptionType(), message="Invalid data type: '#arguments.type#'", detail="The data type must be one of 'string', 'numeric', 'date' 'boolean' or 'blank'." );
/* Note: To properly apply date/number formatting:
- cell type must be CELL_TYPE_NUMERIC
Expand All @@ -2487,24 +2498,32 @@ component accessors="true"{
arguments.cell.setCellType( arguments.cell.CellType.NUMERIC );
arguments.cell.setCellValue( JavaCast( "double", Val( arguments.value ) ) );
return;
case "date":
case "date": case "time":
//handle empty strings which can't be treated as dates
if( !Len( Trim( arguments.value ) ) ){
arguments.cell.setCellType( arguments.cell.CellType.BLANK ); //no need to set the value: it will be blank
return;
}
var cellFormat = getDateTimeValueFormat( arguments.value );
// if time has been specified, trust the source to avoid incorrect time value parsing
if( arguments.type == "time" ){
var dateTimeValue = arguments.value;
var cellFormat = this.getDateFormats().TIME; //don't include the epoch date in the display
}
else {
var dateTimeValue = ParseDateTime( arguments.value );
var cellFormat = getDateTimeValueFormat( dateTimeValue );// check if DATE, TIME or TIMESTAMP
}
var formatter = arguments.workbook.getCreationHelper().createDataFormat();
//Use setCellStyleProperty() which will try to re-use an existing style rather than create a new one for every cell which may breach the 4009 styles per wookbook limit
getCellUtil().setCellStyleProperty( arguments.cell, getCellUtil().DATA_FORMAT, formatter.getFormat( JavaCast( "string", cellFormat ) ) );
arguments.cell.setCellType( arguments.cell.CellType.NUMERIC );
/* Excel's uses a different epoch than CF (1900-01-01 versus 1899-12-30). "Time" only values will not display properly without special handling - */
if( cellFormat EQ this.getDateFormats().TIME ){
arguments.value = TimeFormat( arguments.value, "HH:MM:SS" );
arguments.cell.setCellValue( getDateUtil().convertTime( arguments.value ) );
/* Excel uses a different epoch than CF (1900-01-01 versus 1899-12-30). "Time" only values will not display properly without special handling */
if( arguments.type == "time" || isTimeOnlyValue( dateTimeValue ) ){
dateTimeValue = dateTimeValue.Add( "d", 2 );//shift the epoch forward to match Excel's
var javaDate = dateTimeValue.from( dateTimeValue.toInstant() );// dateUtil needs a java date
dateTimeValue = ( getDateUtil().getExcelDate( javaDate ) -1 );//Convert to Excel's double value for dates, minus the 1 complete day to leave the day fraction (= time value)
}
else
arguments.cell.setCellValue( ParseDateTime( arguments.value ) );
arguments.cell.setCellValue( dateTimeValue );
return;
case "boolean":
//handle empty strings/nulls which can't be treated as booleans
Expand Down
34 changes: 33 additions & 1 deletion test/specs/addRows.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe( "addRows",function(){
workbook = s.new();
var dateValue = CreateDate( 2015, 04, 12 );
var timeValue = CreateTime( 1, 0, 0 );
var dateTimeValue = createDateTime( 2015, 04, 12, 1, 0, 0 );
var dateTimeValue = CreateDateTime( 2015, 04, 12, 1, 0, 0 );
var dataAsArray = [ [ dateValue, timeValue, dateTimeValue ] ];
s.addRows( workbook, dataAsArray );
actual = s.sheetToQuery( workbook );
Expand All @@ -138,6 +138,38 @@ describe( "addRows",function(){
expect( s.getCellType( workbook, 1, 3 ) ).toBe( "numeric" );
});
it( "Formats time and timestamp values correctly when custom mask includes fractions of a second",function() {
dateFormats = {
TIME: "hh:mm:ss.000"
,TIMESTAMP: "yyyy-mm-dd hh:mm:ss.000"
};
var s = newSpreadsheetInstance( dateFormats: dateFormats );
/*
ACF doesn't support milliseconds, ie:
var timeValue = CreateTime( 1, 0, 0, 999 );
var dateTimeValue = CreateDateTime( 2015, 04, 12, 1, 0, 0, 999 );
So use java to create datetime objects including milliseconds
*/
var timeValue = CreateObject( "java", "java.util.Date" ).init( JavaCast( "long", 360000999 ) );
var dateTimeValue = CreateObject( "java", "java.util.Date" ).init( JavaCast( "long", 1428796800999 ) );
var data = QueryNew( "column1,column2", "Time,Timestamp", [ [ timeValue, dateTimeValue ] ] );
s.addRows( variables.workbook, data );
expectedTimeValue = data.column1[ 1 ].TimeFormat( "hh:nn:ss:l" );
expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd hh:nn:ss:l" );
actual = s.sheetToQuery( workbook );
actualTimeValue = actual.column1[ 1 ];
actualDateTimeValue = actual.column2[ 1 ];
//array data
var workbook = s.new();
var dataAsArray = [ [ timeValue, dateTimeValue ] ];
s.addRows( workbook, dataAsArray );
expectedTimeValue = data.column1[ 1 ].TimeFormat( "hh:nn:ss:l" );
expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd hh:nn:ss:l" );
actual = s.sheetToQuery( workbook );
actualTimeValue = actual.column1[ 1 ];
actualDateTimeValue = actual.column2[ 1 ];
});
it( "Adds zeros as zeros, not booleans",function(){
var data = QueryNew( "column1", "Integer", [ [ 0 ] ] );
s.addRows( workbook, data );
Expand Down

0 comments on commit aaf40ba

Please sign in to comment.