diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cac2751..7ae762d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,42 +1,65 @@ name: Run test suite -on: - [ workflow_dispatch, workflow_call ] +on: [workflow_dispatch, workflow_call] jobs: tests: runs-on: ubuntu-latest strategy: matrix: - cfengine: [ "lucee@6", "lucee@7", "adobe@2025" ] + cfengine: ["lucee@6", "lucee@7", "adobe@2025", "boxlang"] steps: - - name: Checkout Repository - uses: actions/checkout@v4 + - name: Checkout Repository + uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v3 - with: - java-version: '21' - distribution: 'temurin' + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: "21" + distribution: "temurin" - - name: Install the ortus security key - run: curl -fsSl https://downloads.ortussolutions.com/debs/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/ortussolutions.gpg > /dev/null + - name: Setup CommandBox CLI + uses: Ortus-Solutions/setup-commandbox@7be599028dcf77743854b2f6c720b15316f39be2 # v2.0.1 + with: + version: latest - - name: Add the commandbox source - run: echo "deb [signed-by=/usr/share/keyrings/ortussolutions.gpg] https://downloads.ortussolutions.com/debs/noarch /" | sudo tee /etc/apt/sources.list.d/commandbox.list + - name: Install dependencies + run: box install - - name: Update apt and install commandbox - run: sudo apt-get update && sudo apt-get install apt-transport-https commandbox + - name: Start a server + run: box server start cfengine=${{ matrix.cfengine }} port=8080 host=0.0.0.0 - - name: Install dependencies - run: box install + - name: Install ACF packages + if: ${{ matrix.cfengine == 'adobe@2025' }} + run: box run-script cfpmInstall - - name: Start a server - run: box server start cfengine=${{ matrix.cfengine }} port=8080 + - name: Install BX packages + if: ${{ matrix.cfengine == 'boxlang' }} + run: box run-script boxLangInstall - - name: Install ACF packages - if: ${{ matrix.cfengine == 'adobe@2025' }} - run: box run-script cfpmInstall + - name: Run TestBox Tests + id: run_tests + run: | + mkdir -p test/results + box testbox run runner="http://localhost:8080/test/index.cfm" outputFile="test/results/test-results" outputFormats="json" + continue-on-error: true + timeout-minutes: 10 - - name: Run TestBox Tests - run: box testbox run runner="http://localhost:8080/test/index.cfm" + - name: Display Test Results + if: always() + run: | + if [ -f "test/results/test-results.json" ]; then + echo "=== Test Results Summary (${{ matrix.cfengine }}) ===" + cat test/results/test-results.json | jq -r ' + "Total Bundles: \(.totalBundles)", + "Total Suites: \(.totalSuites)", + "Total Specs: \(.totalSpecs)", + "Total Pass: \(.totalPass)", + "Total Fail: \(.totalFail)", + "Total Error: \(.totalError)", + "Total Skipped: \(.totalSkipped)" + ' + else + echo "No test results found!" + ls -la test/results/ || echo "Results directory doesn't exist" + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29ef299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +testbox/ +server.json +test/results/* +bx-image/ \ No newline at end of file diff --git a/Spreadsheet.cfc b/Spreadsheet.cfc index 666c41b..1a8ed3e 100644 --- a/Spreadsheet.cfc +++ b/Spreadsheet.cfc @@ -78,7 +78,7 @@ component accessors="true"{ } private string function determineEngine(){ - if( server.coldfusion.productname == "ColdFusion Server" ) + if( (server?.coldfusion?.productname ?: '') == "ColdFusion Server" ) return "ColdFusion"; // PLEASE NOTE: BOXLANG SUPPORT IS CURRENTLY EXPERIMENTAL AND INCOMPLETE (only circa 75% of tests pass). if( server.KeyExists( "boxlang" ) ) @@ -110,7 +110,7 @@ component accessors="true"{ private void function initializeDateFormats( struct dateFormats ){ variables.dateFormats = getDateHelper().defaultFormats(); - if( arguments.KeyExists( "dateFormats" ) ) + if( arguments.KeyExists( "dateFormats" ) && !isNull(arguments.dateFormats)) setDateFormats( arguments.dateFormats ); } @@ -123,7 +123,7 @@ component accessors="true"{ if( getIsBoxlang() ) variables.loadJavaClassesUsing = "dynamicPath"; //configurable - if( arguments.KeyExists( "loadJavaClassesUsing" ) ) + if( arguments.KeyExists( "loadJavaClassesUsing" ) && !isNull(arguments.loadJavaClassesUsing)) variables.loadJavaClassesUsing = getClassHelper().validateLoadingMethod( arguments.loadJavaClassesUsing ); if( ListFindNoCase( "dynamicPath,javaSettings,classPath", variables.loadJavaClassesUsing ) ) return; @@ -133,7 +133,7 @@ component accessors="true"{ } variables.javaLoaderName = "spreadsheetLibraryClassLoader-#this.getVersion()#-#Hash( GetCurrentTemplatePath() )#"; // Option to use the dot path of an existing javaloader installation to save duplication - if( arguments.KeyExists( "javaLoaderDotPath" ) ) + if( arguments.KeyExists( "javaLoaderDotPath" ) && !isNull( arguments.javaLoaderDotPath ) ) variables.javaLoaderDotPath = arguments.javaLoaderDotPath; } @@ -196,7 +196,7 @@ component accessors="true"{ var spreadsheetBundles = ArrayFilter( allBundles, function( bundle ){ return ( bundle.getSymbolicName() == this.getOsgiLibBundleSymbolicName() ); }); - if( arguments.KeyExists( "version" ) ){ + if( arguments.KeyExists( "version" ) && !isNull( arguments.version ) ){ getOsgiLoader().uninstallBundle( this.getOsgiLibBundleSymbolicName(), arguments.version ); return this; } @@ -259,7 +259,7 @@ component accessors="true"{ ,string datatype ){ var sheet = getSheetHelper().getActiveSheet( arguments.workbook ); - var rowIndex = arguments.KeyExists( "startRow" )? ( arguments.startRow -1 ): 0; + var rowIndex = ( arguments.KeyExists( "startRow" ) && !isNull( arguments.startRow ) )? ( arguments.startRow -1 ): 0; var columnIndex = getColumnHelper().getNewColumnIndex( sheet, rowIndex, arguments.startColumn?:0 ); if( arguments.autoSize ) var columnNumber = ( columnIndex +1 ); //stash the starting column number @@ -269,7 +269,7 @@ component accessors="true"{ if( rowIndex > getSheetHelper().getLastRowIndex( sheet ) || IsNull( row ) ) row = getRowHelper().createRow( arguments.workbook, rowIndex ); // NB: row.getLastCellNum() returns the cell index PLUS ONE or -1 if not found - var insertRequired = ( arguments.KeyExists( "startColumn" ) && arguments.insert && ( columnIndex < row.getLastCellNum() ) ); + var insertRequired = ( arguments.KeyExists( "startColumn" ) && !isNull( arguments.startColumn ) && arguments.insert && ( columnIndex < row.getLastCellNum() ) ); if( insertRequired ) getColumnHelper().shiftColumnsRightStartingAt( columnIndex, row, arguments.workbook ); var cellValueArgs = { @@ -277,7 +277,7 @@ component accessors="true"{ ,cell: getCellHelper().createCell( row, columnIndex ) ,value: cellValue }; - if( arguments.KeyExists( "datatype" ) ) + if( arguments.KeyExists( "datatype" ) && !isNull( arguments.datatype ) ) cellValueArgs.type = arguments.datatype; getCellHelper().setCellValueAsType( argumentCollection=cellValueArgs ); rowIndex++; @@ -305,12 +305,14 @@ component accessors="true"{ ,numeric topRow //top row visible in bottom pane ){ var sheet = getSheetHelper().getActiveSheet( arguments.workbook ); - if( arguments.KeyExists( "leftmostColumn" ) && !arguments.KeyExists( "topRow" ) ) + var leftmostColumnProvided = ( arguments.KeyExists( "leftmostColumn" ) && !isNull( arguments.leftmostColumn ) ); + var topRowProvided = ( arguments.KeyExists( "topRow" ) && !isNull( arguments.topRow ) ); + if( leftmostColumnProvided && !topRowProvided ) arguments.topRow = arguments.freezeRow; - if( arguments.KeyExists( "topRow" ) && !arguments.KeyExists( "leftmostColumn" ) ) + if( topRowProvided && !leftmostColumnProvided ) arguments.leftmostColumn = arguments.freezeColumn; /* createFreezePane() operates on the logical row/column numbers as opposed to physical, so no need for n-1 stuff here */ - if( !arguments.KeyExists( "leftmostColumn" ) ){ + if( !leftmostColumnProvided && !topRowProvided ){ sheet.createFreezePane( JavaCast( "int", arguments.freezeColumn ), JavaCast( "int", arguments.freezeRow ) ); return this; } @@ -335,13 +337,13 @@ component accessors="true"{ if( ( numberOfAnchorCoordinates != 4 ) && ( numberOfAnchorCoordinates != 8 ) ) Throw( type=this.getExceptionType() & ".invalidAnchorArgument", message="Invalid anchor argument", detail="The anchor argument must be a comma-delimited list of integers with either 4 or 8 elements" ); var args = { workbook: arguments.workbook }; - if( arguments.KeyExists( "image" ) ) + if( arguments.KeyExists( "image" ) && !isNull( arguments.image ) ) args.image = arguments.image;//new alias instead of filepath/imageData - if( arguments.KeyExists( "filepath" ) ) + if( arguments.KeyExists( "filepath" ) && !isNull( arguments.filepath ) ) args.image = arguments.filepath; - if( arguments.KeyExists( "imageData" ) ) + if( arguments.KeyExists( "imageData" ) && !isNull( arguments.imageData ) ) args.image = arguments.imageData; - if( arguments.KeyExists( "imageType" ) ) + if( arguments.KeyExists( "imageType" ) && !isNull( arguments.imageType ) ) args.imageType = arguments.imageType; if( !args.KeyExists( "image" ) ) Throw( type=this.getExceptionType() & ".missingImageArgument", message="Missing image path or object", detail="Please supply either the 'filepath' or 'imageData' argument" ); @@ -411,11 +413,11 @@ component accessors="true"{ ,boolean ignoreQueryColumnDataTypes=false ,struct datatypes ){ - if( arguments.KeyExists( "row" ) && ( arguments.row <= 0 ) ) + if( arguments.KeyExists( "row" ) && !isNull(arguments.row) && ( arguments.row <= 0 ) ) Throw( type=this.getExceptionType() & ".invalidRowArgument", message="Invalid row value", detail="The value for row must be greater than or equal to 1." ); - if( arguments.KeyExists( "column" ) && ( arguments.column <= 0 ) ) + if( arguments.KeyExists( "column" ) && !isNull( arguments.column ) && ( arguments.column <= 0 ) ) Throw( type=this.getExceptionType() & ".invalidColumnArgument", message="Invalid column value", detail="The value for column must be greater than or equal to 1." ); - if( !arguments.insert && !arguments.KeyExists( "row") ) + if( !arguments.insert && ( !arguments.KeyExists( "row") || isNull( arguments.row ) ) ) Throw( type=this.getExceptionType() & ".missingRowArgument", message="Missing row value", detail="To replace a row using 'insert', please specify the row to replace." ); var dataIsQuery = IsQuery( arguments.data ); var dataIsArray = IsArray( arguments.data ); @@ -430,11 +432,11 @@ component accessors="true"{ Throw( type=this.getExceptionType() & ".invalidDataArgument", message="Invalid data argument", detail="Data passed as an array must be an array of arrays, one per row" ); var sheet = getSheetHelper().getActiveSheet( arguments.workbook ); var nextRowIndex = getSheetHelper().getNextEmptyRowIndex( sheet ); - var insertAtRowIndex = arguments.KeyExists( "row" )? arguments.row -1: nextRowIndex; - if( arguments.KeyExists( "row" ) && ( arguments.row <= nextRowIndex ) && arguments.insert ) + var insertAtRowIndex = ( arguments.KeyExists( "row" ) && !isNull( arguments.row ) )? arguments.row -1: nextRowIndex; + if( arguments.KeyExists( "row" ) && !isNull( arguments.row ) && ( arguments.row <= nextRowIndex ) && arguments.insert ) shiftRows( arguments.workbook, arguments.row, nextRowIndex, totalRows ); var currentRowIndex = insertAtRowIndex; - var overrideDataTypes = arguments.KeyExists( "datatypes" ); + var overrideDataTypes = arguments.KeyExists( "datatypes" ) && !isNull( arguments.datatypes ); if( arguments.autoSizeColumns && isStreamingXmlFormat( arguments.workbook ) ) getSheetHelper().getActiveSheet( arguments.workbook ).trackAllColumnsForAutoSizing(); /* this will affect performance but is needed for autoSizeColumns to work properly with SXSSF: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/streaming/SXSSFSheet.html#trackAllColumnsForAutoSizing */ @@ -591,7 +593,7 @@ component accessors="true"{ Throw( type=this.getExceptionType() & ".missingRequiredArgument", message="Missing required argument", detail="Please provide either a csv string (csv), or the path of a file containing one (filepath)." ); if( csvIsString && csvIsFile ) Throw( type=this.getExceptionType() & ".invalidArgumentCombination", message="Mutually exclusive arguments: 'csv' and 'filepath'", detail="Only one of either 'filepath' or 'csv' arguments may be provided." ); - if( IsStruct( arguments.queryColumnTypes ) && !arguments.firstRowIsHeader && !arguments.KeyExists( "queryColumnNames" ) ) + if( IsStruct( arguments.queryColumnTypes ) && !arguments.firstRowIsHeader && ( !arguments.KeyExists( "queryColumnNames" ) || isNull( arguments.queryColumnNames ) ) ) Throw( type=this.getExceptionType() & ".invalidArgumentCombination", message="Invalid argument 'queryColumnTypes'.", detail="When specifying 'queryColumnTypes' as a struct you must also set the 'firstRowIsHeader' argument to true OR provide 'queryColumnNames'" ); var format = getCsvHelper().getFormat( arguments.delimiter?:"" ); var parsed = csvIsFile? @@ -599,7 +601,7 @@ component accessors="true"{ getCsvHelper().parseFromString( arguments.csv, arguments.trim, format ); var data = parsed.data; var maxColumnCount = parsed.maxColumnCount; - if( arguments.KeyExists( "queryColumnNames" ) && arguments.queryColumnNames.Len() ){ + if( arguments.KeyExists( "queryColumnNames" ) && !isNull( arguments.queryColumnNames ) && arguments.queryColumnNames.Len() ){ var columnNames = arguments.queryColumnNames; var parsedQueryColumnTypes = getQueryHelper().parseQueryColumnTypesArgument( arguments.queryColumnTypes, columnNames, maxColumnCount, data ); return getQueryHelper()._QueryNew( columnNames, parsedQueryColumnTypes, data, arguments.makeColumnNamesSafe ); @@ -674,7 +676,7 @@ component accessors="true"{ arguments.filename = filenameWithoutExtension & "." & extension; var binary = readBinary( arguments.workbook ); cleanUpStreamingXml( arguments.workbook ); - if( !arguments.KeyExists( "contentType" ) ) + if( !arguments.KeyExists( "contentType" ) || isNull( arguments.contentType ) ) arguments.contentType = isXmlFormat( arguments.workbook )? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "application/msexcel"; getFileHelper().downloadBinaryVariable( binary, arguments.filename, arguments.contentType ); } @@ -730,10 +732,10 @@ component accessors="true"{ ,streamingWindowSize: arguments.streamingWindowSize ,ignoreQueryColumnDataTypes: arguments.ignoreQueryColumnDataTypes }; - if( arguments.KeyExists( "datatypes" ) ) + if( arguments.KeyExists( "datatypes" ) && !isNull( arguments.datatypes ) ) binaryFromQueryArgs.datatypes = arguments.datatypes; var binary = binaryFromQuery( argumentCollection=binaryFromQueryArgs ); - if( !arguments.KeyExists( "contentType" ) ) + if( !arguments.KeyExists( "contentType" ) || isNull( arguments.contentType ) ) arguments.contentType = arguments.xmlFormat? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "application/msexcel"; getFileHelper().downloadBinaryVariable( binary, arguments.filename, arguments.contentType ); } @@ -762,7 +764,7 @@ component accessors="true"{ ){ arguments = getFormatHelper().checkFormatArguments( argumentCollection=arguments ); var cell = getCellHelper().initializeCell( arguments.workbook, arguments.row, arguments.column ); - if( arguments.KeyExists( "cellStyle" ) ){ + if( arguments.KeyExists( "cellStyle" ) && !isNull( arguments.cellStyle ) ){ getFormatHelper().setCellStyle( cell, arguments.cellStyle ); return this; } @@ -900,11 +902,13 @@ component accessors="true"{ public any function getCellComment( required workbook, numeric row, numeric column ){ // returns struct OR array of structs - if( arguments.KeyExists( "row" ) && !arguments.KeyExists( "column" ) ) + var rowProvided = ( arguments.KeyExists( "row" ) && !isNull( arguments.row ) ); + var columnProvided = ( arguments.KeyExists( "column" ) && !isNull( arguments.column ) ); + if( rowProvided && !columnProvided ) Throw( type=this.getExceptionType() & ".invalidArgumentCombination", message="Invalid argument combination", detail="If you specify the row you must also specify the column" ); - if( arguments.KeyExists( "column" ) && !arguments.KeyExists( "row" ) ) + if( columnProvided && !rowProvided ) Throw( type=this.getExceptionType() & ".invalidArgumentCombination", message="Invalid argument combination", detail="If you specify the column you must also specify the row" ); - if( !arguments.KeyExists( "row" ) ) + if( !rowProvided ) return getCellComments( arguments.workbook );// row and column weren't provided so return all the comments as an array of structs var cell = getCellHelper().getCellAt( arguments.workbook, arguments.row, arguments.column ); if( IsNull( cell ) ) @@ -972,7 +976,7 @@ component accessors="true"{ } public any function getCellFormula( required workbook, numeric row, numeric column ){ - if( !arguments.KeyExists( "row" ) || !arguments.KeyExists( "column" ) ) + if( !( arguments.KeyExists( "row" ) && !isNull( arguments.row ) ) || !( arguments.KeyExists( "column" ) && !isNull( arguments.column ) ) ) return getSheetHelper().getAllSheetFormulas( arguments.workbook ); var cell = getCellHelper().getCellAt( arguments.workbook, arguments.row, arguments.column ); if( IsNull( cell ) ) @@ -984,7 +988,10 @@ component accessors="true"{ public string function getCellHyperLink( required workbook, required numeric row, required numeric column ){ var cell = getCellHelper().initializeCell( arguments.workbook, arguments.row, arguments.column ); - return cell.getHyperLink()?.getAddress()?:""; + var hyperLink = cell.getHyperLink(); + if( IsNull( hyperLink ) ) + return ""; + return hyperLink.getAddress(); } public string function getCellType( required workbook, required numeric row, required numeric column ){ @@ -1004,7 +1011,7 @@ component accessors="true"{ } public numeric function getColumnCount( required workbook, sheetNameOrNumber ){ - if( arguments.KeyExists( "sheetNameOrNumber" ) ) + if( arguments.KeyExists( "sheetNameOrNumber" ) && !isNull( arguments.sheetNameOrNumber ) ) getSheetHelper().setActiveSheetNameOrNumber( argumentCollection=arguments ); var result = 0; var rowIterator = getSheetHelper().getActiveSheetRowIterator( arguments.workbook ); @@ -1026,7 +1033,7 @@ component accessors="true"{ } public numeric function getLastRowNumber( required workbook, sheetNameOrNumber ){ - if( arguments.KeyExists( "sheetNameOrNumber" ) ) + if( arguments.KeyExists( "sheetNameOrNumber" ) && !isNull( arguments.sheetNameOrNumber ) ) getSheetHelper().setActiveSheetNameOrNumber( argumentCollection=arguments ); var sheet = getSheetHelper().getActiveSheet( arguments.workbook ); var lastRowIndex = getSheetHelper().getLastRowIndex( sheet ); @@ -1042,7 +1049,7 @@ component accessors="true"{ } public boolean function getRecalculateFormulasOnNextOpen( required workbook, string sheetName ){ - if( arguments.KeyExists( "sheetName" ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ){ var sheet = getSheetHelper().getSheetByName( arguments.workbook, arguments.sheetName ); return sheet.getForceFormulaRecalculation(); } @@ -1205,7 +1212,7 @@ component accessors="true"{ if( arguments.streamingXml && !arguments.xmlFormat ) arguments.xmlFormat = true; var createArgs.type = getWorkbookHelper().typeFromArguments( arguments.xmlFormat, arguments.streamingXml ); - if( arguments.KeyExists( "streamingWindowSize" ) ) + if( arguments.KeyExists( "streamingWindowSize" ) && !isNull( arguments.streamingWindowSize ) ) createArgs.streamingWindowSize = arguments.streamingWindowSize; var workbook = getWorkbookHelper().createWorkBook( argumentCollection=createArgs ); getSheetHelper().validateSheetName( arguments.sheetName ); @@ -1282,30 +1289,30 @@ component accessors="true"{ getExceptionHelper().throwExceptionIFreadFormatIsInvalid( argumentCollection=arguments ); getSheetHelper().throwErrorIFSheetNameAndNumberArgumentsBothPassed( argumentCollection=arguments ); getFileHelper().throwErrorIFfileNotExists( arguments.src ); - var passwordProtected = ( arguments.KeyExists( "password") && !arguments.password.Trim().IsEmpty() ); + var passwordProtected = ( arguments.KeyExists( "password" ) && !isNull( arguments.password ) && !arguments.password.Trim().IsEmpty() ); var workbook = passwordProtected? getWorkbookHelper().workbookFromFile( arguments.src, arguments.password ): getWorkbookHelper().workbookFromFile( arguments.src ); - if( arguments.KeyExists( "sheetName" ) ) + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ) setActiveSheet( workbook=workbook, sheetName=arguments.sheetName ); - if( !arguments.KeyExists( "format" ) ) + if( !arguments.KeyExists( "format" ) || isNull( arguments.format ) ) return workbook; var args = { workbook: workbook }; - if( arguments.KeyExists( "sheetName" ) ) + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ) args.sheetName = arguments.sheetName; - if( arguments.KeyExists( "sheetNumber" ) ) + if( arguments.KeyExists( "sheetNumber" ) && !isNull( arguments.sheetNumber ) ) args.sheetNumber = arguments.sheetNumber; - if( arguments.KeyExists( "headerRow" ) ){ + if( arguments.KeyExists( "headerRow" ) && !isNull( arguments.headerRow ) ){ args.headerRow = arguments.headerRow; args.includeHeaderRow = arguments.includeHeaderRow; } - if( arguments.KeyExists( "rows" ) ) + if( arguments.KeyExists( "rows" ) && !isNull( arguments.rows ) ) args.rows = arguments.rows; - if( arguments.KeyExists( "columns" ) ) + if( arguments.KeyExists( "columns" ) && !isNull( arguments.columns ) ) args.columns = arguments.columns; - if( arguments.KeyExists( "columnNames" ) ) + if( arguments.KeyExists( "columnNames" ) && !isNull( arguments.columnNames ) ) args.columnNames = arguments.columnNames; // columnNames is what cfspreadsheet action="read" uses - else if( arguments.KeyExists( "queryColumnNames" ) ) + else if( arguments.KeyExists( "queryColumnNames" ) && !isNull( arguments.queryColumnNames ) ) args.columnNames = arguments.queryColumnNames;// accept better alias `queryColumnNames` to match csvToQuery - if( ( arguments.format == "query" ) && arguments.KeyExists( "queryColumnTypes" ) ){ + if( ( arguments.format == "query" ) && arguments.KeyExists( "queryColumnTypes" ) && !isNull( arguments.queryColumnTypes ) ){ args.queryColumnTypes = arguments.queryColumnTypes; getQueryHelper().throwErrorIFinvalidQueryColumnTypesArgument( argumentCollection=args ); } @@ -1369,7 +1376,7 @@ component accessors="true"{ if( arguments.KeyExists( "streamingReaderOptions" ) ) //support legacy naming arguments.streamingOptions = arguments.streamingReaderOptions; var builderOptions = arguments.streamingReaderOptions?:{}; - if( arguments.KeyExists( "password" ) ) + if( arguments.KeyExists( "password" ) && !isNull( arguments.password ) ) builderOptions.password = arguments.password; var sheetToQueryArgs = { includeBlankRows: arguments.includeBlankRows @@ -1378,17 +1385,17 @@ component accessors="true"{ ,makeColumnNamesSafe: arguments.makeColumnNamesSafe ,returnVisibleValues = arguments.returnVisibleValues }; - if( arguments.KeyExists( "sheetName" ) ) + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ) sheetToQueryArgs.sheetName = arguments.sheetName; - if( arguments.KeyExists( "sheetNumber" ) ) + if( arguments.KeyExists( "sheetNumber" ) && !isNull( arguments.sheetNumber ) ) sheetToQueryArgs.sheetNumber = arguments.sheetNumber; - if( arguments.KeyExists( "headerRow" ) ){ + if( arguments.KeyExists( "headerRow" ) && !isNull( arguments.headerRow ) ){ sheetToQueryArgs.headerRow = arguments.headerRow; sheetToQueryArgs.includeHeaderRow = arguments.includeHeaderRow; } - if( arguments.KeyExists( "queryColumnNames" ) ) + if( arguments.KeyExists( "queryColumnNames" ) && !isNull( arguments.queryColumnNames ) ) sheetToQueryArgs.columnNames = arguments.queryColumnNames; - if( ( arguments.format == "query" ) && arguments.KeyExists( "queryColumnTypes" ) ){ + if( ( arguments.format == "query" ) && arguments.KeyExists( "queryColumnTypes" ) && !isNull( arguments.queryColumnTypes ) ){ sheetToQueryArgs.queryColumnTypes = arguments.queryColumnTypes; getQueryHelper().throwErrorIFinvalidQueryColumnTypesArgument( argumentCollection=sheetToQueryArgs ); } @@ -1465,7 +1472,7 @@ component accessors="true"{ public Spreadsheet function setActiveSheet( required workbook, string sheetName, numeric sheetNumber ){ getSheetHelper().validateSheetNameOrNumberWasProvided( argumentCollection=arguments ); - if( arguments.KeyExists( "sheetName" ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ){ getSheetHelper().validateSheetExistsWithName( arguments.workbook, arguments.sheetName ); arguments.sheetNumber = ( arguments.workbook.getSheetIndex( JavaCast( "string", arguments.sheetName ) ) + 1 ); } @@ -1551,7 +1558,7 @@ component accessors="true"{ getHyperLinkHelper().throwErrorIfTooltipAndWorkbookIsXls( argumentCollection=arguments ); var cell = getCellHelper().initializeCell( arguments.workbook, arguments.row, arguments.column ); getHyperLinkHelper().addHyperLinkToCell( cell=cell, argumentCollection=arguments ); - if( arguments.KeyExists( "cellValue" ) ) + if( arguments.KeyExists( "cellValue" ) && !isNull( arguments.cellValue ) ) getCellHelper().setCellValueAsType( arguments.workbook, cell, arguments.cellValue ); formatCell( arguments.workbook, arguments.format, arguments.row, arguments.column ); return this; @@ -1578,10 +1585,10 @@ component accessors="true"{ ,cell: getCellHelper().initializeCell( arguments.workbook, arguments.row, arguments.column ) ,value: arguments.value }; - if( arguments.KeyExists( "datatype" ) ) + if( arguments.KeyExists( "datatype" ) && !isNull( arguments.datatype ) ) args.type = arguments.datatype; //support legacy argument name - if( arguments.KeyExists( "type" ) ) + if( arguments.KeyExists( "type" ) && !isNull( arguments.type ) ) args.type = arguments.type; getCellHelper().setCellValueAsType( argumentCollection=args ); return this; @@ -1604,9 +1611,9 @@ component accessors="true"{ sheet.setAutoBreaks( JavaCast( "boolean", arguments.state ) ); //seems dependent on this matching if( !arguments.state ) return this; - if( arguments.KeyExists( "pagesWide" ) && IsValid( "integer", arguments.pagesWide ) ) + if( arguments.KeyExists( "pagesWide" ) && !isNull( arguments.pagesWide ) && IsValid( "integer", arguments.pagesWide ) ) sheet.getPrintSetup().setFitWidth( JavaCast( "short", arguments.pagesWide ) ); - if( arguments.KeyExists( "pagesWide" ) && IsValid( "integer", arguments.pagesHigh ) ) + if( arguments.KeyExists( "pagesHigh" ) && !isNull( arguments.pagesHigh ) && IsValid( "integer", arguments.pagesHigh ) ) sheet.getPrintSetup().setFitHeight( JavaCast( "short", arguments.pagesHigh ) ); return this; } @@ -1672,7 +1679,7 @@ component accessors="true"{ } public Spreadsheet function setRecalculateFormulasOnNextOpen( required workbook, boolean value=true, string sheetName ){ - if( arguments.KeyExists( "sheetName" ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ){ var sheet = getSheetHelper().getSheetByName( arguments.workbook, arguments.sheetName ); sheet.setForceFormulaRecalculation( JavaCast( "boolean", arguments.value ) ); return this; @@ -1847,11 +1854,11 @@ component accessors="true"{ firstRowIsHeader: arguments.firstRowIsHeader ,trim: arguments.trim }; - if( arguments.KeyExists( "csv" ) ) + if( arguments.KeyExists( "csv" ) && !isNull( arguments.csv ) ) conversionArgs.csv = arguments.csv; - if( arguments.KeyExists( "filepath" ) ) + if( arguments.KeyExists( "filepath" ) && !isNull( arguments.filepath ) ) conversionArgs.filepath = arguments.filepath; - if( arguments.KeyExists( "delimiter" ) ) + if( arguments.KeyExists( "delimiter" ) && !isNull( arguments.delimiter ) ) conversionArgs.delimiter = arguments.delimiter; var data = csvToQuery( argumentCollection=conversionArgs ); return workbookFromQuery( @@ -1880,7 +1887,7 @@ component accessors="true"{ ,ignoreQueryColumnDataTypes: arguments.ignoreQueryColumnDataTypes ,autoSizeColumns: arguments.autoSizeColumns }; - if( arguments.KeyExists( "datatypes" ) ) + if( arguments.KeyExists( "datatypes" ) && !isNull( arguments.datatypes ) ) addRowsArgs.datatypes = arguments.datatypes; if( arguments.addHeaderRow ){ var columns = getQueryHelper()._QueryColumnArray( arguments.data ); @@ -1903,7 +1910,7 @@ component accessors="true"{ ){ if( !arguments.overwrite && FileExists( arguments.filepath ) ) getExceptionHelper().throwFileExistsException( arguments.filepath ); - var passwordProtect = ( arguments.KeyExists( "password" ) && !arguments.password.Trim().IsEmpty() ); + var passwordProtect = ( arguments.KeyExists( "password" ) && !isNull( arguments.password ) && !arguments.password.Trim().IsEmpty() ); if( passwordProtect && isBinaryFormat( arguments.workbook ) ) Throw( type=this.getExceptionType() & ".invalidSpreadsheetType", message="Whole file password protection is not supported for binary workbooks", detail="Password protection only works with XML ('xlsx') workbooks." ); try{ @@ -1950,7 +1957,7 @@ component accessors="true"{ ,streamingWindowSize: arguments.streamingWindowSize ,ignoreQueryColumnDataTypes: arguments.ignoreQueryColumnDataTypes }; - if( arguments.KeyExists( "datatypes" ) ) + if( arguments.KeyExists( "datatypes" ) && !isNull( arguments.datatypes ) ) workbookFromQueryArgs.datatypes = arguments.datatypes; var workbook = workbookFromQuery( argumentCollection=workbookFromQueryArgs ); // force to .xlsx if appropriate diff --git a/box.json b/box.json index bc860a0..7316f2d 100644 --- a/box.json +++ b/box.json @@ -15,7 +15,8 @@ }, "engines" : [ { "type" : "lucee", "version" : ">=5.x.x" }, - { "type" : "adobe", "version" : ">=2021.0.0" } + { "type" : "adobe", "version" : ">=2021.0.0" }, + { "type" : "boxlang", "version": ">=1.13.0+50" } ], "defaultEngine" : "lucee", "license" : [ @@ -29,6 +30,7 @@ "private" : "false", "ignore" : [ ".github", "build", "src", "test", ".gitattributes", "CHANGELOG.md", "README.md" ], "scripts":{ - "cfpmInstall": "cfpm install image" + "cfpmInstall": "cfpm install image", + "boxLangInstall":"install bx-image" } } \ No newline at end of file diff --git a/helpers/cell.cfc b/helpers/cell.cfc index 1e03a86..67ec726 100644 --- a/helpers/cell.cfc +++ b/helpers/cell.cfc @@ -11,7 +11,7 @@ component extends="base"{ any function getDataFormatPropertyType(){ if( IsNull( variables.dataFormatPropertyType ) ) - variables.dataFormatPropertyType = library().createJavaObject( "org.apache.poi.ss.usermodel.CellPropertyType" ).DATA_FORMAT; + variables.dataFormatPropertyType = library().createJavaObject( "org.apache.poi.ss.usermodel.CellPropertyType" ).valueOf( JavaCast( "string", "DATA_FORMAT" ) ); return variables.dataFormatPropertyType; } @@ -24,7 +24,7 @@ component extends="base"{ } boolean function cellIsOfType( required cell, required string type ){ - return arguments.cell.getCellType().Equals( arguments.cell.getCellType()[ arguments.type ] ); + return arguments.cell.getCellType().Equals( arguments.cell.getCellType().valueOf( JavaCast( "string", arguments.type ) ) ); } any function createCell( required row, numeric cellNum=arguments.row.getLastCellNum(), overwrite=true ){ @@ -39,7 +39,10 @@ component extends="base"{ any function getCellAt( required workbook, required numeric rowNumber, required numeric columnNumber ){ var columnIndex = ( arguments.columnNumber -1 ); - return getRowHelper().getRowFromActiveSheet( arguments.workbook, arguments.rowNumber )?.getCell( JavaCast( "int", columnIndex ) ); + var row = getRowHelper().getRowFromActiveSheet( arguments.workbook, arguments.rowNumber ); + if( IsNull( row ) ) + return; + return row.getCell( JavaCast( "int", columnIndex ) ); } any function getCellFormulaValue( required workbook, required cell, boolean forceEvaluation=false ){ @@ -94,7 +97,7 @@ component extends="base"{ if( Trim( arguments.value ).IsEmpty() ) return setEmptyValue( arguments.cell ); var validCellTypes = getDataTypeHelper().validCellOverrideTypes().Append( "blank" ); - if( !arguments.KeyExists( "type" ) ) //autodetect type + if( !arguments.KeyExists( "type" ) || isNull( arguments.type ) ) //autodetect type arguments.type = getDataTypeHelper().detectValueDataType( arguments.value ); else if( !validCellTypes.FindNoCase( arguments.type ) ) Throw( type=library().getExceptionType() & ".invalidDatatype", message="Invalid data type: '#arguments.type#'", detail="The data type must be one of the following: #validCellTypes.ToList( ', ' )#." ); @@ -236,9 +239,9 @@ component extends="base"{ } private any function getCachedFormulaValue( required cell ){ - if( arguments.cell.getCachedFormulaResultType().Equals( arguments.cell.getCellType().NUMERIC ) ) - return getCellNumericOrDateValue( arguments.cell ); - if( arguments.cell.getCachedFormulaResultType().Equals( arguments.cell.getCellType().BOOLEAN ) ) + if( arguments.cell.getCachedFormulaResultType().Equals( arguments.cell.getCellType().valueOf( JavaCast( "string", "NUMERIC" ) ) ) ) + return getCellNumericOrDateValue( arguments.cell ); + if( arguments.cell.getCachedFormulaResultType().Equals( arguments.cell.getCellType().valueOf( JavaCast( "string", "BOOLEAN" ) ) ) ) return arguments.cell.getBooleanCellValue(); return getStringValue( arguments.cell ); } diff --git a/helpers/color.cfc b/helpers/color.cfc index 381972a..fe19fdd 100644 --- a/helpers/color.cfc +++ b/helpers/color.cfc @@ -12,7 +12,10 @@ component extends="base"{ array function getRGBFromCellFont( required workbook, required any cellFont ){ if( library().isXmlFormat( arguments.workbook ) ) return getRGBFromXSSFCellFont( arguments.cellFont ); - return arguments.cellFont.getHSSFColor( arguments.workbook )?.getTriplet()?:[]; + var hssfColor = arguments.cellFont.getHSSFColor( arguments.workbook ); + if( IsNull( hssfColor ) ) + return []; + return hssfColor.getTriplet(); } any function getColor( required workbook, required string colorValue ){ @@ -148,9 +151,13 @@ component extends="base"{ } private array function getRGBFromXSSFCellFont( required any cellFont ){ - if( IsNull( arguments.cellFont.getXSSFColor()?.getRGB() ) ) + var xssfColor = arguments.cellFont.getXSSFColor(); + if( IsNull( xssfColor ) ) + return []; + var rgb = xssfColor.getRGB(); + if( IsNull( rgb ) ) return []; - return convertSignedRGBToPositiveTriplet( arguments.cellFont.getXSSFColor().getRGB() ); + return convertSignedRGBToPositiveTriplet( rgb ); } } \ No newline at end of file diff --git a/helpers/comment.cfc b/helpers/comment.cfc index 1a84b41..f3c04bc 100644 --- a/helpers/comment.cfc +++ b/helpers/comment.cfc @@ -23,20 +23,20 @@ component extends="base"{ if( !commentHasFontStyles( arguments.comment ) ) return this; var font = arguments.workbook.createFont(); - if( arguments.comment.KeyExists( "bold" ) ) + if( arguments.comment.KeyExists( "bold" ) && !isNull(arguments.comment.bold)) font.setBold( JavaCast( "boolean", arguments.comment.bold ) ); - if( arguments.comment.KeyExists( "color" ) ) + if( arguments.comment.KeyExists( "color" ) && !isNull(arguments.comment.color)) font.setColor( getColorHelper().getColor( arguments.workbook, arguments.comment.color ) ); - if( arguments.comment.KeyExists( "font" ) ) + if( arguments.comment.KeyExists( "font" ) && !isNull(arguments.comment.front)) font.setFontName( JavaCast( "string", arguments.comment.font ) ); - if( arguments.comment.KeyExists( "italic" ) ) + if( arguments.comment.KeyExists( "italic" ) && !isNull(arguments.comment.italic)) font.setItalic( JavaCast( "string", arguments.comment.italic ) ); - if( arguments.comment.KeyExists( "size" ) ) + if( arguments.comment.KeyExists( "size" ) && !isNull(arguments.comment.size)) font.setFontHeightInPoints( JavaCast( "int", arguments.comment.size ) ); - if( arguments.comment.KeyExists( "strikeout" ) ) + if( arguments.comment.KeyExists( "strikeout" ) && !isNull(arguments.comment.strikeout)) font.setStrikeout( JavaCast( "boolean", arguments.comment.strikeout ) ); - if( arguments.comment.KeyExists( "underline" ) ) - font.setUnderline( JavaCast( "byte", arguments.comment.underline ) ); + if( arguments.comment.KeyExists( "underline" ) && !isNull(arguments.comment.underline)) + font.setUnderline( JavaCast( "byte", booleanFormat(arguments.comment.underline) ? 1 : 0 ) ); arguments.commentString.applyFont( font ); return this; } diff --git a/helpers/csv.cfc b/helpers/csv.cfc index c8e6c6e..0eb443c 100644 --- a/helpers/csv.cfc +++ b/helpers/csv.cfc @@ -1,7 +1,7 @@ component extends="base"{ any function getFormatObject( string type="DEFAULT" ){ - return library().createJavaObject( "org.apache.commons.csv.CSVFormat" )[ JavaCast( "string", arguments.type ) ]; + return library().createJavaObject( "org.apache.commons.csv.CSVFormat" ).valueOf( JavaCast( "string", arguments.type ) ); } boolean function delimiterIsTab( required string delimiter ){ diff --git a/helpers/dataType.cfc b/helpers/dataType.cfc index a868809..5bc8a98 100644 --- a/helpers/dataType.cfc +++ b/helpers/dataType.cfc @@ -38,7 +38,7 @@ component extends="base"{ /* Data type overriding */ any function checkDataTypesArgument( required struct args ){ - if( arguments.args.KeyExists( "datatypes" ) && datatypeOverridesContainInvalidTypes( arguments.args.datatypes ) ) + if( arguments.args.KeyExists( "datatypes" ) && !isNull( arguments.args.datatypes ) && datatypeOverridesContainInvalidTypes( arguments.args.datatypes ) ) Throw( type=library().getExceptionType() & ".invalidDatatype", message="Invalid datatype(s)", detail="One or more of the datatypes specified is invalid. Valid types are #validCellOverrideTypes().ToList( ', ' )# and the columns they apply to should be passed as an array" ); return this; } @@ -47,11 +47,11 @@ component extends="base"{ for( var type in arguments.datatypeOverrides ){ var columnRefs = arguments.datatypeOverrides[ type ]; var totalColumnRefs = columnRefs.Len(); - cfloop( from=1, to=totalColumnRefs, index="local.index" ){ - if( IsNumeric( columnRefs[ index ] ) ) //position already given + for( var i = 1; i <= totalColumnRefs; i++ ){ + if( IsNumeric( columnRefs[ i ] ) ) //position already given continue; - var columnNumber = ArrayFindNoCase( columnNames, columnRefs[ index ] );//ACF won't accept member function on this array for some reason - columnRefs[ index ] = columnNumber; + var columnNumber = ArrayFindNoCase( columnNames, columnRefs[ i ] );//ACF won't accept member function on this array for some reason + columnRefs[ i ] = columnNumber; } arguments.datatypeOverrides[ type ] = columnRefs; } @@ -78,7 +78,7 @@ component extends="base"{ } } // if no override, use an already set default (i.e. query column type) - if( arguments.KeyExists( "defaultType" ) ){ + if( arguments.KeyExists( "defaultType" ) && !isNull( arguments.defaultType ) ){ getCellHelper().setCellValueAsType( arguments.workbook, arguments.cell, arguments.cellValue, arguments.defaultType ); return this; } diff --git a/helpers/date.cfc b/helpers/date.cfc index a883a46..b17eea8 100644 --- a/helpers/date.cfc +++ b/helpers/date.cfc @@ -16,6 +16,15 @@ component extends="base"{ } struct function defaultFormats(){ + if( library().getIsBoxlang() ) { + return { + DATE: "yyyy-MM-dd" + ,DATETIME: "yyyy-MM-dd HH:mm:ss" + ,TIME: "hh:mm:ss" + ,TIMESTAMP: "yyyy-MM-dd HH:mm:ss" + }; + } + return { DATE: "yyyy-mm-dd" ,DATETIME: "yyyy-mm-dd HH:nn:ss" @@ -43,13 +52,18 @@ component extends="base"{ } boolean function isDateObject( required input ){ - return IsInstanceOf( arguments.input, "java.util.Date" ); + if( IsInstanceOf( arguments.input, "java.util.Date" ) ) + return true; + if( library().getIsBoxlang() ) + return IsInstanceOf( arguments.input, "ortus.boxlang.runtime.types.DateTime" ); + return false; } + //TODO improve these imperfect tests! boolean function isDateOnlyValue( required date value ){ if( library().getIsBoxlang() ) - return ( arguments.value.TimeFormat( "hh:mm:ss" ) == "00:00:00" ); + return ( arguments.value.TimeFormat( "HH:mm:ss" ) == "00:00:00" ); var dateOnly = CreateDate( Year( arguments.value ), Month( arguments.value ), Day( arguments.value ) ); return ( DateCompare( arguments.value, dateOnly, "s" ) == 0 ); } @@ -109,10 +123,10 @@ component extends="base"{ return ParseDateTime( arguments.value, "EEE MMM d HH:mm:ss zzz yyyy" ); //e.g. 08:21 if( arguments.value.REFindNoCase( "^\d{2,2}:\d{2,2}$" ) ) - return ParseDateTime( "1899-12-30T#arguments.value#:00Z" ); + return ParseDateTime( "1899-12-30T#arguments.value#:00" ); //e.g. 08:21:30 if( arguments.value.REFindNoCase( "^\d{2,2}:\d{2,2}:\d{2,2}$" ) ) - return ParseDateTime( "1899-12-30T#arguments.value#Z" ); + return ParseDateTime( "1899-12-30T#arguments.value#" ); return ParseDateTime( arguments.value ); } diff --git a/helpers/exception.cfc b/helpers/exception.cfc index ca60fc6..d0c9098 100644 --- a/helpers/exception.cfc +++ b/helpers/exception.cfc @@ -18,7 +18,7 @@ component extends="base"{ } void function throwExceptionIFreadFormatIsInvalid(){ - if( arguments.KeyExists( "format" ) && !ListFindNoCase( "query,array,arrayOfStructs,html,csv", arguments.format ) ) + if( arguments.KeyExists( "format" ) && !isNull( arguments.format ) && !ListFindNoCase( "query,array,arrayOfStructs,html,csv", arguments.format ) ) Throw( type=library().getExceptionType() & ".invalidReadFormat", message="Invalid format", detail="Supported formats are: 'query', 'array', 'arrayOfStructs', 'html' and 'csv'" ); } @@ -27,8 +27,12 @@ component extends="base"{ for some reason ACF won't match the exception type as a catch() arg here, i.e. catch( com.github.pjfanning.xlsx.exceptions.ReadException exception ){} hence using an if-test */ - if( arguments.exception.type == "com.github.pjfanning.xlsx.exceptions.ReadException" ) + if( + arguments.exception.type == "com.github.pjfanning.xlsx.exceptions.ReadException" + || library().getIsBoxlang() && arguments.exception.getClass().getName() == "com.github.pjfanning.xlsx.exceptions.ReadException" + ) { Throw( type=library().getExceptionType() & ".invalidSpreadsheetType", message="Invalid spreadsheet file", detail="readLargeFile() and processLargeFile() can only be used with XLSX files. The file you are trying to read does not appear to be an XLSX file." ); + } } void function throwNonExistentRowException( required numeric rowNumber ){ diff --git a/helpers/file.cfc b/helpers/file.cfc index ca0752c..d0d7e16 100644 --- a/helpers/file.cfc +++ b/helpers/file.cfc @@ -14,7 +14,7 @@ component extends="base"{ Throw( type=library().getExceptionType() & ".invalidAlgorithm", message="Invalid algorithm", detail="'#arguments.algorithm#' is not a valid algorithm. Supported algorithms are: #validAlgorithms.ToList( ', ')#" ); lock name="#arguments.filepath#" timeout=5 { var mode = library().createJavaObject( "org.apache.poi.poifs.crypt.EncryptionMode" ); - var info = library().createJavaObject( "org.apache.poi.poifs.crypt.EncryptionInfo" ).init( mode[ arguments.algorithm ] ); + var info = library().createJavaObject( "org.apache.poi.poifs.crypt.EncryptionInfo" ).init( mode.valueOf( JavaCast( "string", arguments.algorithm ) ) ); var encryptor = info.getEncryptor(); encryptor.confirmPassword( JavaCast( "string", arguments.password ) ); try{ diff --git a/helpers/font.cfc b/helpers/font.cfc index dc860cf..671965f 100644 --- a/helpers/font.cfc +++ b/helpers/font.cfc @@ -101,7 +101,7 @@ component extends="base"{ case "bold": return "font-weight:" & ( arguments.styleValue? "bold;": "normal;" ); case "color": - if( !arguments.KeyExists( "workbook" ) ) + if( !arguments.KeyExists( "workbook" ) || isNull( arguments.workbook ) ) Throw( type=library().getExceptionType() & ".missingRequiredArgument", message="Missing required 'workbook' argument", detail="The 'workbook' argument is required when generating color css styles" ); //http://ragnarock99.blogspot.co.uk/2012/04/getting-hex-color-from-excel-cell.html var rgb = arguments.workbook.getCustomPalette().getColor( arguments.styleValue ).getTriplet(); diff --git a/helpers/format.cfc b/helpers/format.cfc index e09adfe..47c4bf1 100644 --- a/helpers/format.cfc +++ b/helpers/format.cfc @@ -34,7 +34,7 @@ component extends="base"{ var newCellStyleForThisWorkbook = workbook.createCellStyle(); newCellStyleForThisWorkbook.cloneStyleFrom( arguments.cellStyle ); arguments.cell.setCellStyle( newCellStyleForThisWorkbook ); - if( !arguments.KeyExists( "format" ) || StructIsEmpty( arguments.format ) ) + if( !arguments.KeyExists( "format" ) || isNull( arguments.format ) || StructIsEmpty( arguments.format ) ) return; var spreadsheetType = library().isXmlFormat( workbook )? "xlsx": "xls"; var cellStyleID = getCellStyleIDfromFormat( arguments.format ); @@ -54,7 +54,7 @@ component extends="base"{ any function buildCellStyle( required workbook, required struct format, existingStyle ){ var cellStyle = arguments.workbook.createCellStyle(); - if( arguments.KeyExists( "existingStyle" ) ) + if( arguments.KeyExists( "existingStyle" ) && !isNull( arguments.existingStyle ) ) cellStyle.cloneStyleFrom( arguments.existingStyle ); for( var setting in arguments.format ) setCellStyleFromFormatSetting( arguments.workbook, cellStyle, arguments.format, setting ); @@ -68,14 +68,14 @@ component extends="base"{ } struct function checkFormatArguments( required workbook, boolean overwriteCurrentStyle=true ){ - if( !arguments.KeyExists( "format" ) && !arguments.KeyExists( "cellStyle" ) ) + if( ( !arguments.KeyExists( "format" ) || isNull( arguments.format ) ) && ( !arguments.KeyExists( "cellStyle" ) || isNull( arguments.cellStyle ) ) ) Throw( type=library().getExceptionType() & ".missingRequiredArgument", message="Missing argument: 'format'", detail="The 'format' argument is required" ); if( arguments.KeyExists( "format" ) && IsStruct( arguments.format ) ) return arguments; //assume a cellStyle object has been supplied either as cellStyle or format if( !arguments.overwriteCurrentStyle ) Throw( type=library().getExceptionType() & ".invalidArgumentCombination", message="Invalid argument combination", detail="If you supply a 'cellStyle' the 'overwriteCurrentStyle' cannot be false" ); - if( !arguments.KeyExists( "cellStyle" ) ) + if( !arguments.KeyExists( "cellStyle" ) || isNull( arguments.cellStyle ) ) arguments.cellStyle = arguments.format; if( !isValidCellStyleObject( arguments.workbook, arguments.cellStyle ) ) Throw( type=library().getExceptionType() & ".invalidCellStyleArgument", message="Invalid argument", detail="The 'cellStyle' supplied is not a valid POI cellStyle object" ); @@ -185,7 +185,7 @@ component extends="base"{ var settingValue = arguments.format[ arguments.setting ]; switch( arguments.setting ){ case "alignment": - var alignment = arguments.cellStyle.getAlignment()[ JavaCast( "string", UCase( settingValue ) ) ]; + var alignment = arguments.cellStyle.getAlignment().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setAlignment( alignment ); return this; case "bold": @@ -194,7 +194,7 @@ component extends="base"{ arguments.cellStyle.setFont( font ); return this; case "bottomborder": - var borderStyle = arguments.cellStyle.getBorderBottom()[ JavaCast( "string", UCase( settingValue ) ) ]; + var borderStyle = arguments.cellStyle.getBorderBottom().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setBorderBottom( borderStyle ); return this; case "bottombordercolor": @@ -213,7 +213,7 @@ component extends="base"{ arguments.cellStyle.setFillForegroundColor( getColorHelper().getColor( arguments.workbook, settingValue ) ); // make sure we always apply a fill pattern or the color will not be visible if( !arguments.format.KeyExists( "fillpattern" ) ){ - var fillpattern = arguments.cellStyle.getFillPattern()[ JavaCast( "string", "SOLID_FOREGROUND" ) ]; + var fillpattern = arguments.cellStyle.getFillPattern().valueOf( JavaCast( "string", "SOLID_FOREGROUND" ) ); arguments.cellStyle.setFillPattern( fillpattern ); } return this; @@ -221,7 +221,7 @@ component extends="base"{ //ACF docs list "nofill" as opposed to "no_fill" if( settingValue == "nofill" ) settingValue = "NO_FILL"; - var fillpattern = arguments.cellStyle.getFillPattern()[ JavaCast( "string", UCase( settingValue ) ) ]; + var fillpattern = arguments.cellStyle.getFillPattern().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setFillPattern( fillpattern ); return this; case "font": @@ -249,7 +249,7 @@ component extends="base"{ arguments.cellStyle.setFont( font ); return this; case "leftborder": - var borderStyle = arguments.cellStyle.getBorderLeft()[ JavaCast( "string", UCase( settingValue ) ) ]; + var borderStyle = arguments.cellStyle.getBorderLeft().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setBorderLeft( borderStyle ); return this; case "leftbordercolor": @@ -263,7 +263,7 @@ component extends="base"{ arguments.cellStyle.setQuotePrefixed( JavaCast( "boolean", settingValue ) ); return this; case "rightborder": - var borderStyle = arguments.cellStyle.getBorderRight()[ JavaCast( "string", UCase( settingValue ) ) ]; + var borderStyle = arguments.cellStyle.getBorderRight().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setBorderRight( borderStyle ); return this; case "rightbordercolor": @@ -281,7 +281,7 @@ component extends="base"{ arguments.cellStyle.setWrapText( JavaCast( "boolean", settingValue ) ); return this; case "topborder": - var borderStyle = arguments.cellStyle.getBorderTop()[ JavaCast( "string", UCase( settingValue ) ) ]; + var borderStyle = arguments.cellStyle.getBorderTop().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setBorderTop( borderStyle ); return this; case "topbordercolor": @@ -296,7 +296,7 @@ component extends="base"{ arguments.cellStyle.setFont( font ); return this; case "verticalalignment": - var alignment = arguments.cellStyle.getVerticalAlignment()[ JavaCast( "string", UCase( settingValue ) ) ]; + var alignment = arguments.cellStyle.getVerticalAlignment().valueOf( JavaCast( "string", UCase( settingValue ) ) ); arguments.cellStyle.setVerticalAlignment( alignment ); } return this; diff --git a/helpers/hyperLink.cfc b/helpers/hyperLink.cfc index 2f30612..9d9f2d0 100644 --- a/helpers/hyperLink.cfc +++ b/helpers/hyperLink.cfc @@ -8,16 +8,16 @@ component extends="base"{ } any function throwErrorIfTooltipAndWorkbookIsXls( required workbook ){ - if( arguments.KeyExists( "tooltip" ) && !library().isXmlFormat( arguments.workbook ) ) + if( arguments.KeyExists( "tooltip" ) && !isNull( arguments.tooltip ) && !library().isXmlFormat( arguments.workbook ) ) Throw( type=library().getExceptionType() & ".invalidSpreadsheetType", message="Invalid spreadsheet type", detail="Hyperlink tooltips can only be added to XLSX spreadsheets." ); return this; } any function addHyperLinkToCell( required cell, required workbook, required string link, required string type, string tooltip ){ var hyperlinkType = library().createJavaObject( "org.apache.poi.common.usermodel.HyperlinkType" ); - var hyperLink = arguments.workbook.getCreationHelper().createHyperlink( hyperlinkType[ arguments.type ] ); + var hyperLink = arguments.workbook.getCreationHelper().createHyperlink( hyperlinkType.valueOf( JavaCast( "string", arguments.type ) ) ); hyperLink.setAddress( JavaCast( "string", arguments.link ) ); - if( arguments.KeyExists( "tooltip" ) ) + if( arguments.KeyExists( "tooltip" ) && !isNull( arguments.tooltip ) ) hyperLink.setTooltip( JavaCast( "string", arguments.tooltip ) ); arguments.cell.setHyperlink( hyperLink ); return this; diff --git a/helpers/image.cfc b/helpers/image.cfc index e69fd23..ec46994 100644 --- a/helpers/image.cfc +++ b/helpers/image.cfc @@ -7,7 +7,7 @@ component extends="base"{ ){ // TODO image objects don't always work, depending on how they're created: POI accepts it but the image is not displayed (broken) var imageArgumentIsObject = IsImage( arguments.image ); - if( imageArgumentIsObject && !arguments.KeyExists( "imageType" ) ) + if( imageArgumentIsObject && ( !arguments.KeyExists( "imageType" ) || isNull( arguments.imageType ) ) ) Throw( type=library().getExceptionType() & ".invalidArgumentCombination", message="Invalid argument combination", detail="If you specify an image object, you must also provide the imageType argument" ); var imageArgumentIsFile = ( !imageArgumentIsObject && IsSimpleValue( arguments.image ) && FileExists( arguments.image ) ); if( !imageArgumentIsObject && IsSimpleValue( arguments.image ) && !imageArgumentIsFile ) diff --git a/helpers/query.cfc b/helpers/query.cfc index d60141b..802614e 100644 --- a/helpers/query.cfc +++ b/helpers/query.cfc @@ -69,14 +69,40 @@ component extends="base"{ } array function getQueryColumnTypeToCellTypeMappings( required query query ){ - // extract the query columns and data types var metadata = GetMetaData( arguments.query ); - // assign default formats based on the data type of each column - for( var columnMetadata in metadata ) - mapQueryColumnTypeToCellType( columnMetadata ); + + // Boxlang returns column metadata as a struct + if(library().getIsBoxlang()) { + return parseMetadata(metadata); + } + else { + for( var columnMetadata in metadata ) { + mapQueryColumnTypeToCellType( columnMetadata ); + } + } + return metadata; } + // Convert Boxlang metadata to same formatted array and match ACF/Lucee keys + function parseMetadata(required any metadata) { + if(library().getIsBoxlang()) { + return metaData.columnMetadata.reduce((result, col, val) => { + var curr = { + cellDataType: '', + isCaseSensitive: false, + name: val.name, + typeName: val.type + }; + mapQueryColumnTypeToCellType(curr); + result.append(curr); + return result; + }, []); + } + return metaData; + } + + string function parseQueryColumnTypesArgument( required any queryColumnTypes ,required array columnNames @@ -115,7 +141,7 @@ component extends="base"{ } void function throwErrorIFinvalidQueryColumnTypesArgument( required queryColumnTypes ){ - if( IsStruct( arguments.queryColumnTypes ) && !arguments.KeyExists( "headerRow" ) && !arguments.KeyExists( "columnNames" ) ) + if( IsStruct( arguments.queryColumnTypes ) && ( !arguments.KeyExists( "headerRow" ) || isNull( arguments.headerRow ) ) && ( !arguments.KeyExists( "columnNames" ) || isNull( arguments.columnNames ) ) ) Throw( type=library().getExceptionType() & ".invalidQueryColumnTypesArgument", message="Invalid argument 'queryColumnTypes'.", detail="When specifying 'queryColumnTypes' as a struct you must also specify the 'headerRow' or provide 'columnNames'" ); } @@ -234,13 +260,25 @@ component extends="base"{ } private query function QueryNewBoxlang( required array columnNames, required string columnTypeList, required array data ){ - var result = QueryNew( arguments.columnNames.ToList(), arguments.columnTypeList ); - arguments.data.Each( function( row ){ + var columnTypes = arguments.columnTypeList.Len() ? ListToArray( arguments.columnTypeList ) : []; + + // Create empty query + var result = QueryNew( "" ); + + // Loop over each column and add + arguments.columnNames.Each( ( colName, i ) => { + QueryAddColumn( result, colName, ( columnTypes.Len() >= i ? columnTypes[ i ] : "VARCHAR" ), [] ); + }); + + // Add each data row + arguments.data.Each( (row) => { QuerySetRow( result, 0, convertJavaArrayToCFMLArray( row ) ); }); + return replaceNullsWithEmptyValues( result ); } + private array function convertJavaArrayToCFMLArray( required any value ){ //Boxlang BIFs may not accept java string arrays if( arguments.value.getClass().getName() != "[Ljava.lang.String;" ) diff --git a/helpers/row.cfc b/helpers/row.cfc index f135971..46c5836 100644 --- a/helpers/row.cfc +++ b/helpers/row.cfc @@ -110,7 +110,7 @@ component extends="base"{ any function createRow( required workbook, numeric rowIndex, boolean overwrite=true ){ // get existing row (if any) var sheet = getSheetHelper().getActiveSheet( arguments.workbook ); - if( !arguments.KeyExists( "rowIndex" ) ) + if( !arguments.KeyExists( "rowIndex" ) || isNull( arguments.rowIndex ) ) arguments.rowIndex = getSheetHelper().getNextEmptyRowIndex( sheet ); var row = sheet.getRow( JavaCast( "int", arguments.rowIndex ) ); if( arguments.overwrite && !IsNull( row ) ) @@ -300,18 +300,22 @@ component extends="base"{ var cell = getCellHelper().createCell( arguments.newRow, cellIndex, false ); var cellValue = rowData[ queryColumn.name ]; if( arguments.ignoreQueryColumnDataTypes ){ - if( overrideDataTypes ) - getDataTypeHelper().setCellDataTypeWithOverride( arguments.workbook, cell, cellValue, cellIndex, arguments.datatypes ); - else + if( overrideDataTypes ) { + getDataTypeHelper().setCellDataTypeWithOverride( arguments.workbook, cell, cellValue, cellIndex, arguments.datatypes ); + } + else { getCellHelper().setCellValueAsType( arguments.workbook, cell, cellValue ); + } cellIndex++; continue; } var cellValueType = getDataTypeHelper().getCellValueTypeFromQueryColumnType( queryColumn.cellDataType, cellValue ); - if( overrideDataTypes ) - getDataTypeHelper().setCellDataTypeWithOverride( arguments.workbook, cell, cellValue, cellIndex, arguments.datatypes, cellValueType ); - else + if( overrideDataTypes ) { + getDataTypeHelper().setCellDataTypeWithOverride( workbook=arguments.workbook, cell=cell, cellValue=cellValue, cellIndex=cellIndex, datatypeOverrides=arguments.datatypes, defaultType=cellValueType ); + } + else { getCellHelper().setCellValueAsType( arguments.workbook, cell, cellValue, cellValueType ); + } cellIndex++; } return this; diff --git a/helpers/sheet.cfc b/helpers/sheet.cfc index 9548636..886c42e 100644 --- a/helpers/sheet.cfc +++ b/helpers/sheet.cfc @@ -1,7 +1,7 @@ component extends="base"{ string function createOrValidateSheetName( required workbook ){ - if( !arguments.KeyExists( "sheetName" ) ) + if( !arguments.KeyExists( "sheetName" ) || isNull( arguments.sheetName ) ) return generateUniqueSheetName( arguments.workbook ); validateSheetName( arguments.sheetName ); return arguments.sheetName; @@ -95,10 +95,10 @@ component extends="base"{ } numeric function getSheetNumberFromArguments( required workbook, string sheetName, numeric sheetNumber ){ - if( !arguments.KeyExists( "sheetName" ) && !arguments.KeyExists( "sheetNumber" ) ) + if( ( !arguments.KeyExists( "sheetName" ) || isNull( arguments.sheetName ) ) && ( !arguments.KeyExists( "sheetNumber" ) || isNull( arguments.sheetNumber ) ) ) return getActiveSheetNumber( arguments.workbook ); validateSheetNameOrNumberWasProvided( argumentCollection=arguments ); - if( arguments.KeyExists( "sheetName" ) && Len( Trim( arguments.sheetName ) ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) && Len( Trim( arguments.sheetName ) ) ){ validateSheetExistsWithName( arguments.workbook, arguments.sheetName ); arguments.sheetNumber = ( arguments.workbook.getSheetIndex( JavaCast( "string", arguments.sheetName ) ) + 1 ); } @@ -116,7 +116,7 @@ component extends="base"{ } struct function info( required workbook, numeric sheetNumber ){ - if( !arguments.KeyExists( "sheetNumber" ) ) + if( !arguments.KeyExists( "sheetNumber" ) || isNull( arguments.sheetNumber ) ) arguments.sheetNumber = ( arguments.workbook.getActiveSheetIndex() +1 ); var sheet = getSheetByNumber( argumentCollection=arguments ); var isXlsx = library().isXmlFormat( arguments.workbook ); @@ -157,7 +157,7 @@ component extends="base"{ var validStates = [ "HIDDEN", "VERY_HIDDEN", "VISIBLE" ]; if( !validStates.Find( arguments.visibility ) ) Throw( type=this.getExceptionType() & ".invalidVisibilityArgument", message="Invalid visibility argument: '#arguments.visibility#'", detail="The visibility must be one of the following: #validStates.ToList( ', ' )#." ); - var visibilityEnum = library().createJavaObject( "org.apache.poi.ss.usermodel.SheetVisibility" )[ JavaCast( "string", arguments.visibility ) ]; + var visibilityEnum = library().createJavaObject( "org.apache.poi.ss.usermodel.SheetVisibility" ).valueOf( JavaCast( "string", arguments.visibility ) ); var sheetIndex = ( arguments.sheetNumber -1 ); arguments.workbook.setSheetVisibility( sheetIndex, visibilityEnum ); /* POI Docs: "Please note that the sheet currently set as active sheet (sheet 0 in a newly created workbook or the one set via setActiveSheet()) cannot be hidden." */ @@ -172,7 +172,7 @@ component extends="base"{ boolean function sheetExists( required workbook, string sheetName, numeric sheetNumber ){ validateSheetNameOrNumberWasProvided( argumentCollection=arguments ); - if( arguments.KeyExists( "sheetName" ) ) + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ) arguments.sheetNumber = ( getSheetIndexFromName( arguments.workbook, arguments.sheetName ) +1 ); //the position is valid if it's an integer between 1 and the total number of sheets in the workbook if( arguments.sheetNumber && ( arguments.sheetNumber == Round( arguments.sheetNumber ) ) && ( arguments.sheetNumber <= arguments.workbook.getNumberOfSheets() ) ) @@ -228,11 +228,11 @@ component extends="base"{ ,boolean forceColumnGeneration=false ){ var result = [ columns: [], data: [] ];//ordered struct - if( arguments.KeyExists( "sheetName" ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ){ validateSheetExistsWithName( arguments.workbook, arguments.sheetName ); arguments.sheetNumber = ( getSheetIndexFromName( arguments.workbook, arguments.sheetName ) +1 ); } - else if( !arguments.KeyExists( "sheetNumber" ) ) + else if( !arguments.KeyExists( "sheetNumber" ) || isNull( arguments.sheetNumber ) ) arguments.sheetNumber = getFirstVisibleSheetNumber( arguments.workbook ); if( arguments.sheetNumber == 0 ) return result;//no visible sheets @@ -270,11 +270,11 @@ component extends="base"{ ,boolean makeColumnNamesSafe=false ,boolean returnVisibleValues=false ){ - if( arguments.KeyExists( "sheetName" ) ){ + if( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) ){ validateSheetExistsWithName( arguments.workbook, arguments.sheetName ); arguments.sheetNumber = ( getSheetIndexFromName( arguments.workbook, arguments.sheetName ) +1 ); } - else if( !arguments.KeyExists( "sheetNumber" ) ) + else if( !arguments.KeyExists( "sheetNumber" ) || isNull( arguments.sheetNumber ) ) arguments.sheetNumber = getFirstVisibleSheetNumber( arguments.workbook ); if( arguments.sheetNumber == 0 ) return QueryNew( "" );//no visible sheets @@ -438,11 +438,11 @@ component extends="base"{ } private boolean function sheetNameArgumentWasProvided(){ - return ( arguments.KeyExists( "sheetName" ) && Len( arguments.sheetName ) ); + return ( arguments.KeyExists( "sheetName" ) && !isNull( arguments.sheetName ) && Len( arguments.sheetName ) ); } private boolean function sheetNumberArgumentWasProvided(){ - return ( arguments.KeyExists( "sheetNumber" ) && Len( arguments.sheetNumber ) ); + return ( arguments.KeyExists( "sheetNumber" ) && !isNull( arguments.sheetNumber ) && Len( arguments.sheetNumber ) ); } private any function throwErrorIFSheetNameAndNumberArgumentsBothMissing(){ @@ -530,7 +530,7 @@ component extends="base"{ ){ var sheet = { includeHeaderRow: arguments.includeHeaderRow - ,hasHeaderRow: ( arguments.KeyExists( "headerRow" ) && Val( arguments.headerRow ) ) + ,hasHeaderRow: ( arguments.KeyExists( "headerRow" ) && !isNull( arguments.headerRow ) && Val( arguments.headerRow ) ) ,includeBlankRows: arguments.includeBlankRows ,includeHiddenRows: arguments.includeHiddenRows ,columnNames: [] @@ -539,10 +539,10 @@ component extends="base"{ ,data: [] ,hasRows: false }; - if( arguments.KeyExists( "columnNames" ) && arguments.columnNames.Len() ) + if( arguments.KeyExists( "columnNames" ) && !isNull( arguments.columnNames ) && arguments.columnNames.Len() ) sheet.columnNames = IsArray( arguments.columnNames )? arguments.columnNames: arguments.columnNames.ListToArray(); sheet.headerRowIndex = sheet.hasHeaderRow? ( arguments.headerRow -1 ): -1; - if( arguments.KeyExists( "columns" ) ){ + if( arguments.KeyExists( "columns" ) && !isNull( arguments.columns ) ){ sheet.columnRanges = getRangeHelper().extractRanges( arguments.columns, arguments.workbook, "column" ); sheet.totalColumnCount = getColumnHelper().columnCountFromRanges( sheet.columnRanges ); } @@ -556,7 +556,7 @@ component extends="base"{ ,includeRichTextFormatting: arguments.includeRichTextFormatting ,returnVisibleValues: arguments.returnVisibleValues }; - if( arguments.KeyExists( "rows" ) ) + if( arguments.KeyExists( "rows" ) && !isNull( arguments.rows ) ) populateDataArgs.rows = arguments.rows; populateSheetData( argumentCollection=populateDataArgs ); } diff --git a/helpers/workbook.cfc b/helpers/workbook.cfc index 85c2190..82655f1 100644 --- a/helpers/workbook.cfc +++ b/helpers/workbook.cfc @@ -19,7 +19,7 @@ component extends="base"{ try{ var factory = library().createJavaObject( "org.apache.poi.ss.usermodel.WorkbookFactory" ); var file = CreateObject( "java", "java.io.FileInputStream" ).init( arguments.path ); - if( arguments.KeyExists( "password" ) ) + if( arguments.KeyExists( "password" ) && !isNull( arguments.password ) ) return factory.create( file, arguments.password ); return factory.create( file ); } @@ -51,7 +51,7 @@ component extends="base"{ if( library().isSpreadsheetObject( arguments.workbookOrPath ) ) return arguments.workbookOrPath; var args = { path: arguments.workbookOrPath }; - if( arguments.KeyExists( "password" ) ) + if( arguments.KeyExists( "password" ) && !isNull( arguments.password ) ) args.password = arguments.password; return workbookFromFile( argumentCollection=args ); } diff --git a/objects/ConditionalFormatting.cfc b/objects/ConditionalFormatting.cfc index 24fb6f4..b0da94a 100644 --- a/objects/ConditionalFormatting.cfc +++ b/objects/ConditionalFormatting.cfc @@ -73,7 +73,10 @@ component{ else setFormulaRule(); setRuleFormat(); - variables.sheetConditionalFormatting.addConditionalFormatting( [ variables.cellRangeAddress ], variables.conditionalFormattingRule ); + var reflectArray = CreateObject( "java", "java.lang.reflect.Array" ); + var addressArray = reflectArray.newInstance( variables.cellRangeAddress.getClass(), JavaCast( "int", 1 ) ); + reflectArray.set( addressArray, JavaCast( "int", 0 ), variables.cellRangeAddress ); + variables.sheetConditionalFormatting.addConditionalFormatting( addressArray, variables.conditionalFormattingRule ); return this; } diff --git a/objects/ReadCsv.cfc b/objects/ReadCsv.cfc index ae8a5eb..73064d4 100644 --- a/objects/ReadCsv.cfc +++ b/objects/ReadCsv.cfc @@ -140,8 +140,9 @@ component extends="BaseCsv" accessors="true"{ } private void function equalizeColumnLengths( required struct result ){ - arguments.result.data.Each( function( row, index ){ - ArrayResize( result.data[ index ], variables.maxNumberOfColumns );//don't scope arguments within closure + arguments.result.data.Each( ( row, index ) => { + while( ArrayLen( result.data[ index ] ) < variables.maxNumberOfColumns ) + result.data[ index ].Append( "" ); }); } diff --git a/objects/SpreadsheetChainable.cfc b/objects/SpreadsheetChainable.cfc index c7d3ffe..7dc124c 100644 --- a/objects/SpreadsheetChainable.cfc +++ b/objects/SpreadsheetChainable.cfc @@ -513,7 +513,7 @@ component{ ,boolean makeColumnNamesSafe=false ,boolean returnVisibleValues=false ){ - if( arguments.KeyExists( "format" ) ) + if( arguments.KeyExists( "format" ) && !isNull( arguments.format ) ) return variables.library.read( argumentCollection=arguments ); variables.workbook = variables.library.read( argumentCollection=arguments ); return this; diff --git a/objects/WriteCsv.cfc b/objects/WriteCsv.cfc index f3d2401..3d2372f 100644 --- a/objects/WriteCsv.cfc +++ b/objects/WriteCsv.cfc @@ -52,8 +52,13 @@ component extends="BaseCsv" accessors="true"{ Throw( type=variables.library.getExceptionType() & ".missingDataForCsv", message="Missing data", detail="Please specify the data you want to write using '.fromData( data )'" ); var appendable = newAppendableBuffer(); printTo( appendable ); - if( IsNull( variables.filepath ) ) + if( IsNull( variables.filepath ) ) { + if(getLibrary().getIsBoxlang()) { + // Boxlang somehow modified the .toString() call, this will ensure we aren't trimming whitespace + return appendable.substring( JavaCast( "int", 0 ), JavaCast( "int", appendable.length() ) ); + } return appendable.toString(); + } return this; } @@ -71,6 +76,15 @@ component extends="BaseCsv" accessors="true"{ printFromArray( printer ); } finally{ + // Boxlang needs another explicit row call to have a trailing new row at the end of a file + if( + !isNull(variables.filepath) + && local.KeyExists( "printer" ) + && getLibrary().getIsBoxlang() + ) { + printRowFromArray([], printer); + } + if( local.KeyExists( "printer" ) ) printer.close( JavaCast( "boolean", true ) ); } @@ -173,8 +187,8 @@ component extends="BaseCsv" accessors="true"{ return arguments.row; } - private string function formatDateString( required string value ){ - if( !variables.library.getDateHelper().isDateObject( arguments.value ) ) + private string function formatDateString( required any value ){ + if( !variables.library.getDateHelper().isDateObject( arguments.value )) return arguments.value; return DateTimeFormat( arguments.value, variables.library.getDateFormats().DATETIME ); } diff --git a/test/Application.cfc b/test/Application.cfc index 81bbd14..b7aa12b 100644 --- a/test/Application.cfc +++ b/test/Application.cfc @@ -4,4 +4,5 @@ component{ this.applicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ); variables.relativePathToRoot = "../"; this.mappings[ "/root" ] = GetDirectoryFromPath( GetCurrentTemplatePath() ) & relativePathToRoot; + this.mappings[ "/testbox" ] = GetDirectoryFromPath( GetCurrentTemplatePath() ) & relativePathToRoot & '/testbox'; } \ No newline at end of file diff --git a/test/index.cfm b/test/index.cfm index fd22cc5..8626482 100644 --- a/test/index.cfm +++ b/test/index.cfm @@ -2,7 +2,7 @@ paths = [ "root.test.suite" ]; try{ testRunner = New testbox.system.TestBox( paths ); - WriteOutput( testRunner.run() ); + WriteOutput( testRunner.run(reporter=url?.reporter ?: 'simple') ); } catch( any exception ){ WriteDump( exception ); diff --git a/test/specs/addRows.cfm b/test/specs/addRows.cfm index 359b162..7a7ecc5 100644 --- a/test/specs/addRows.cfm +++ b/test/specs/addRows.cfm @@ -183,8 +183,8 @@ describe( "addRows", ()=>{ 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 ] ] ); - var expectedTimeValue = data.column1[ 1 ].TimeFormat( "hh:nn:ss:l" ); - var expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd hh:nn:ss:l" ); + var expectedTimeValue = data.column1[ 1 ].TimeFormat( "#TIME_WITH_MILLISECONDS_MASK#" ); + var expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd #TIME_WITH_MILLISECONDS_MASK#" ); workbooks.Each( ( wb )=>{ s.addRows( wb, data ); var actual = s.getSheetHelper().sheetToQuery( wb ); @@ -195,8 +195,8 @@ describe( "addRows", ()=>{ var workbooks = [ s.newXls(), s.newXlsx() ]; workbooks.Each( ( wb )=>{ s.addRows( wb, dataAsArray ); - expectedTimeValue = data.column1[ 1 ].TimeFormat( "hh:nn:ss:l" ); - expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd hh:nn:ss:l" ); + expectedTimeValue = data.column1[ 1 ].TimeFormat( "#TIME_WITH_MILLISECONDS_MASK#" ); + expectedDateTimeValue = data.column2[ 1 ].DateTimeFormat( "yyyy-mm-dd #TIME_WITH_MILLISECONDS_MASK#" ); actual = s.getSheetHelper().sheetToQuery( wb ); actualTimeValue = actual.column1[ 1 ]; actualDateTimeValue = actual.column2[ 1 ]; diff --git a/test/specs/cellFormula.cfm b/test/specs/cellFormula.cfm index e478206..d089748 100644 --- a/test/specs/cellFormula.cfm +++ b/test/specs/cellFormula.cfm @@ -158,7 +158,7 @@ describe( "cellFormula", ()=>{ it( "Can be configured to throw an exception on any formula evaluation error", ()=>{ workbooks.Each( ( wb )=>{ - expect( ( wb )=>{ + expect( ()=>{ newSpreadsheetInstance() .setThrowExceptionOnFormulaError( true ) .setCellFormula( wb, "SUS(A1:A2)", 3, 1 ) @@ -167,7 +167,7 @@ describe( "cellFormula", ()=>{ .toThrow( type="cfsimplicity.spreadsheet.failedFormula" ); }) workbooks.Each( ( wb )=>{ - expect( ( wb )=>{ + expect( ()=>{ newSpreadsheetInstance() .setThrowExceptionOnFormulaError( true ) .setCellValue( wb, 0, 2, 1 ) @@ -177,7 +177,7 @@ describe( "cellFormula", ()=>{ .toThrow( type="cfsimplicity.spreadsheet.failedFormula" ); }) workbooks.Each( ( wb )=>{ - expect( ( wb )=>{ + expect( ()=>{ newSpreadsheetInstance() .setThrowExceptionOnFormulaError( true ) .setCellValue( wb, 0, 2, 1 ) diff --git a/test/specs/cellValue.cfm b/test/specs/cellValue.cfm index 485752e..bb8747a 100644 --- a/test/specs/cellValue.cfm +++ b/test/specs/cellValue.cfm @@ -37,7 +37,7 @@ describe( "cellValue", ()=>{ var value = CreateDate( 2015, 04, 12 ); workbooks.Each( ( wb )=>{ s.setCellValue( wb, value, 1, 1 ); - var expected = DateFormat( value, "yyyy-mm-dd" ); + var expected = DateFormat( value, s.getDateFormats().DATE ); var actual = s.getCellValue( wb, 1, 1 ); expect( actual ).toBe( expected ); expect( s.getCellType( wb, 1, 1 ) ).toBe( "numeric" ); @@ -191,7 +191,7 @@ describe( "cellValue", ()=>{ workbooks.Each( ( wb )=>{ s.setCellValue( wb, value, 1, 1, "date" ); var actual = s.getCellValue( wb, 1, 1 ); - expect( DateFormat( actual, "yyyy-mm-dd" ) ).toBe( "1990-01-01" ); + expect( DateFormat( actual, s.getDateFormats().DATE ) ).toBe( "1990-01-01" ); expect( s.getCellType( wb, 1, 1 ) ).toBe( "numeric" );// dates are numeric in Excel }) }) diff --git a/test/specs/csvToQuery.cfm b/test/specs/csvToQuery.cfm index 2816ab9..0200701 100644 --- a/test/specs/csvToQuery.cfm +++ b/test/specs/csvToQuery.cfm @@ -191,10 +191,10 @@ describe( "csvToQuery", ()=>{ it( "allows the query column types to be manually set using a list", ()=>{ var csv = '1,1.1,"string",#_CreateTime( 1, 0, 0 )#'; var q = s.csvToQuery( csv=csv, queryColumnTypes="Integer,Double,VarChar,Time" ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) @@ -202,10 +202,10 @@ describe( "csvToQuery", ()=>{ var csv = 'integer,double,"string column",time#newline#1,1.1,string,12:00'; var columnTypes = { "string column": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.csvToQuery( csv=csv, queryColumnTypes="Integer,Double,VarChar,Time", firstRowIsHeader=true ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) @@ -214,41 +214,41 @@ describe( "csvToQuery", ()=>{ var columnNames = [ "integer", "double", "string column", "time" ]; var columnTypes = { "string": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.csvToQuery( csv=csv, queryColumnTypes=columnTypes, queryColumnNames=columnNames ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) it( "allows the query column types to be automatically set", ()=>{ var csv = '1,1.1,"string",2021-03-10 12:00:00'; var q = s.csvToQuery( csv=csv, queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) it( "automatic detecting of query column types ignores blank cells", ()=>{ var csv = ',,,#newline#,2,test,2021-03-10 12:00:00#newline#1,1.1,string,2021-03-10 12:00:00#newline#1,,,'; var q = s.csvToQuery( csv=csv, queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) it( "allows a default type to be set for all query columns", ()=>{ var csv = '1,1.1,"string",#_CreateTime( 1, 0, 0 )#'; var q = s.csvToQuery( csv=csv, queryColumnTypes="VARCHAR" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 2 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 4 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 2 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 4 ].typeName ).toBe( VARCHAR_CHECK ); }) }) diff --git a/test/specs/dateFormats.cfm b/test/specs/dateFormats.cfm index eb02c87..c351be1 100644 --- a/test/specs/dateFormats.cfm +++ b/test/specs/dateFormats.cfm @@ -1,49 +1,42 @@ describe( "dateFormats customisability", ()=>{ + + it( "the default dateFormats can be overridden individually on init", ()=>{ + // Default formats loaded + var defaultFormats = s.getDateHelper().defaultFormats(); local.s = newSpreadsheetInstance(); - var expected = { - DATE: "yyyy-mm-dd" - ,DATETIME: "yyyy-mm-dd HH:nn:ss" - ,TIME: "hh:mm:ss" - ,TIMESTAMP: "yyyy-mm-dd hh:mm:ss" - }; + var expected = defaultFormats var actual = local.s.getDateFormats(); expect( actual ).toBe( expected ); + + // Override date mask pre instance creation local.s = newSpreadsheetInstance( dateFormats={ DATE: "mm/dd/yyyy" } ); - expected = { - DATE: "mm/dd/yyyy" - ,DATETIME: "yyyy-mm-dd HH:nn:ss" - ,TIME: "hh:mm:ss" - ,TIMESTAMP: "yyyy-mm-dd hh:mm:ss" - }; + expected.DATE = "mm/dd/yyyy"; actual = local.s.getDateFormats(); expect( actual ).toBe( expected ); }) it( "the dateFormats can be set post-init", ()=>{ + // Default formats loaded + var defaultFormats = s.getDateHelper().defaultFormats(); local.s = newSpreadsheetInstance(); - var expected = { - DATE: "yyyy-mm-dd" - ,DATETIME: "yyyy-mm-dd HH:nn:ss" - ,TIME: "hh:mm:ss" - ,TIMESTAMP: "yyyy-mm-dd hh:mm:ss" - }; + var expected = defaultFormats var actual = local.s.getDateFormats(); expect( actual ).toBe( expected ); + + // Override date mask post instance creation var customDateFormats = { DATE: "mm/dd/yyyy" }; local.s.setDateFormats( customDateFormats ); - expected = { - DATE: "mm/dd/yyyy" - ,DATETIME: "yyyy-mm-dd HH:nn:ss" - ,TIME: "hh:mm:ss" - ,TIMESTAMP: "yyyy-mm-dd hh:mm:ss" - }; + expected.DATE = customDateFormats.DATE; expect( local.s.getDateFormats() ).toBe( expected ); }) it( "allows the format of date and time values to be customised", ()=>{ + // Formats change between engines + var defaultFormats = s.getDateHelper().defaultFormats(); + variables.workbooks = [ s.newXls(), s.newXlsx() ]; //Dates var dateValue = CreateDate( 2019, 04, 12 ); @@ -51,24 +44,25 @@ describe( "dateFormats customisability", ()=>{ var timestampValue = CreateDateTime( 2019, 04, 12, 1, 5, 5 ); workbooks.Each( ( wb )=>{ s.setCellValue( wb, dateValue, 1, 1 ); - var expected = DateFormat( dateValue, "yyyy-mm-dd" ); + var expected = DateFormat( dateValue, defaultFormats.DATE ); var actual = s.getCellValue( wb, 1, 1 ); expect( actual ).toBe( expected ); //Times s.setCellValue( wb, timeValue, 1, 1 ); - expected = TimeFormat( timeValue, "hh:mm:ss" ); + expected = TimeFormat( timeValue, defaultFormats.TIME ); actual = s.getCellValue( wb, 1, 1 ); expect( actual ).toBe( expected ); //timestamps s.setCellValue( wb, timestampValue, 1, 1 ); - expected = DateTimeFormat( timestampValue, "yyyy-mm-dd hh:nn:ss" ); + expected = DateTimeFormat( timestampValue, defaultFormats.DATETIME ); actual = s.getCellValue( wb, 1, 1 ); expect( actual ).toBe( expected ); - // custom date format - local.s = newSpreadsheetInstance( dateFormats={ DATE="mm/dd/yyyy" } ); + // Custom format (changes between engines) + var customDateFormat = !s.getIsBoxlang() ? "mm/dd/yyyy" : "MM/dd/yyyy"; + local.s = newSpreadsheetInstance( dateFormats={ DATE=customDateFormat } ); s.setCellValue( wb, dateValue, 1, 1 ); - expected = DateFormat( dateValue, "mm/dd/yyyy" ); + expected = DateFormat( dateValue, customDateFormat ); actual = s.getCellValue( wb, 1, 1 ); expect( actual ).toBe( expected ); //custom time format @@ -76,16 +70,18 @@ describe( "dateFormats customisability", ()=>{ s.setCellValue( wb, timeValue, 1, 1 ); expected = TimeFormat( timeValue, "h:m:s" ); actual = s.getCellValue( wb, 1, 1 ); - //custom timestamp format - local.s = newSpreadsheetInstance( dateFormats={ TIMESTAMP="mm/dd/yyyy h:m:s" } ); + //custom timestamp format (changes between engines) + var customTimestampFormat = !s.getIsBoxlang() ? "mm/dd/yyyy h:m:s" : "MM/dd/yyyy h:m:s" + local.s = newSpreadsheetInstance( dateFormats={ TIMESTAMP=customTimestampFormat } ); s.setCellValue( wb, timestampValue, 1, 1 ); - expected = DateTimeFormat( timestampValue, "mm/dd/yyyy h:n:s" ); + expected = DateTimeFormat( timestampValue, customTimestampFormat ); actual = s.getCellValue( wb, 1, 1 ); }) }) it( "Uses the overridden DATETIME format mask when generating CSV and HTML",()=>{ - local.s = newSpreadsheetInstance( dateFormats={ DATETIME="mm/dd/yyyy h:n:s" } ); + var customDateTimeFormat = !s.getIsBoxlang() ? "mm/dd/yyyy h:n:s" : "MM/dd/yyyy h:m:s"; + local.s = newSpreadsheetInstance( dateFormats={ DATETIME=customDateTimeFormat } ); var path = getTestFilePath( "test.xls" ); var actual = s.read( src=path, format="html" ); var expected = "ab104/01/2015 12:0:004/01/2015 1:1:12"; diff --git a/test/specs/read.cfm b/test/specs/read.cfm index 23c7b5b..5ab0e2c 100644 --- a/test/specs/read.cfm +++ b/test/specs/read.cfm @@ -146,7 +146,7 @@ describe( "read", ()=>{ var actual = s.read( src=path, format="array", headerRow=1 ); expect( actual ).toBe( expected ); //query - expected = QueryNew( "firstColumn,column2", "VarChar,VarChar", [ dataRow1, dataRow2 ] ); + expected = normalizeExpectedQuery( QueryNew( "firstColumn,column2", "VarChar,VarChar", [ dataRow1, dataRow2 ] ) ); actual = s.read( src=path, format="query", headerRow=1 ); expect( actual ).toBe( expected ); }) @@ -202,7 +202,7 @@ describe( "read", ()=>{ var actual = s.read( src=path, format="array", headerRow=1 ); expect( actual ).toBe( expected ); //query - expected = QueryNew( columns.ToList(), "VarChar,VarChar", data ); + expected = normalizeExpectedQuery( QueryNew( columns.ToList(), "VarChar,VarChar", data ) ); actual = s.read( src=path, format="query", headerRow=1 ); expect( actual ).toBe( expected ); }) @@ -222,7 +222,7 @@ describe( "read", ()=>{ var actual = s.read( src=path, format="array", headerRow=1, rows=2 ); expect( actual ).toBe( expected ); //query - expected = QueryNew( columns.ToList(), "VarChar,VarChar", [ data[ 1 ] ] ); + expected = normalizeExpectedQuery( QueryNew( columns.ToList(), "VarChar,VarChar", [ data[ 1 ] ] ) ); actual = s.read( src=path, format="query", headerRow=1, rows=2 ); expect( actual ).toBe( expected ); }) @@ -683,7 +683,7 @@ describe( "read", ()=>{ var actual = s.read( src=path, format="array", columnNames=columnNames ); expect( actual ).toBe( expected ); //query - expected = QueryNew( "firstColumn,column2", "VarChar,VarChar", [ dataRow1, dataRow2 ] ); + expected = normalizeExpectedQuery( QueryNew( "firstColumn,column2", "VarChar,VarChar", [ dataRow1, dataRow2 ] ) ); var actual = s.read( src=path, format="query", columnNames=columnNames ); expect( actual ).toBe( expected ); }) diff --git a/test/specs/read.format.query.cfm b/test/specs/read.format.query.cfm index e592fc0..bffb6b6 100644 --- a/test/specs/read.format.query.cfm +++ b/test/specs/read.format.query.cfm @@ -30,10 +30,10 @@ describe( "read: format=query", ()=>{ .addRows( data ) .write( path, true ) var q = s.read( src=path, format="query", queryColumnTypes="Integer,Double,VarChar,Time" ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) }) @@ -47,10 +47,10 @@ describe( "read: format=query", ()=>{ .write( path, true ); var columnTypes = { "string column": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.read( src=path, format="query", queryColumnTypes=columnTypes, headerRow=1 ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) }) @@ -65,10 +65,10 @@ describe( "read: format=query", ()=>{ var columnNames = "integer,double,string column,time"; var columnTypes = { "string": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.read( src=path, format="query", queryColumnTypes=columnTypes, columnNames=columnNames ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) }) @@ -81,10 +81,10 @@ describe( "read: format=query", ()=>{ .addRows( data ) .write( path, true ); var q = s.read( src=path, format="query", queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) }) @@ -102,10 +102,10 @@ describe( "read: format=query", ()=>{ .addRows( data ) .write( path, true ); var q = s.read( src=path, format="query", queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) }) @@ -118,11 +118,11 @@ describe( "read: format=query", ()=>{ .addRows( data ) .write( path, true ); var q = s.read( src=path, format="query", queryColumnTypes="VARCHAR" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 2 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 4 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 2 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 4 ].typeName ).toBe( VARCHAR_CHECK ); }) }) diff --git a/test/specs/readLargeFile.cfm b/test/specs/readLargeFile.cfm index ccb4191..08bdd9e 100644 --- a/test/specs/readLargeFile.cfm +++ b/test/specs/readLargeFile.cfm @@ -319,10 +319,10 @@ describe( "readLargeFile", ()=>{ it( "allows the query column types to be manually set using list", ()=>{ s.newChainable( "xlsx" ).addRow( [ 1, 1.1, "string", _CreateTime( 1, 0, 0 ) ] ).write( tempXlsxPath, true ); var q = s.readLargeFile( src=tempXlsxPath, queryColumnTypes="Integer,Double,VarChar,Time" ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) @@ -332,10 +332,10 @@ describe( "readLargeFile", ()=>{ .write( tempXlsxPath, true ); var columnTypes = { "string column": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.readLargeFile( src=tempXlsxPath, format="query", queryColumnTypes=columnTypes, headerRow=1 ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) @@ -344,20 +344,20 @@ describe( "readLargeFile", ()=>{ var columnNames = "integer,double,string column,time"; var columnTypes = { "string": "VARCHAR", "integer": "INTEGER", "time": "TIME", "double": "DOUBLE" };//not in order var q = s.readLargeFile( src=tempXlsxPath, queryColumnTypes=columnTypes, queryColumnNames=columnNames ); - var columns = GetMetaData( q ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); expect( columns[ 1 ].typeName ).toBe( "INTEGER" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIME" ); }) it( "allows the query column types to be automatically set", ()=>{ s.newChainable( "xlsx" ).addRow( [ 1, 1.1, "string", Now() ] ).write( tempXlsxPath, true ); var q = s.readLargeFile( src=tempXlsxPath, queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) @@ -370,21 +370,21 @@ describe( "readLargeFile", ()=>{ ]; s.newChainable( "xlsx" ).addRows( data ).write( tempXlsxPath, true ); var q = s.readLargeFile( src=tempXlsxPath, queryColumnTypes="auto" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 2 ].typeName ).toBe( "DOUBLE" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 2 ].typeName ).toBe( DOUBLE_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); expect( columns[ 4 ].typeName ).toBe( "TIMESTAMP" ); }) it( "allows a default type to be set for all query columns", ()=>{ s.newChainable( "xlsx" ).addRow( [ 1, 1.1, "string", Now() ] ).write( tempXlsxPath, true ); var q = s.readLargeFile( src=tempXlsxPath, queryColumnTypes="VARCHAR" ); - var columns = GetMetaData( q ); - expect( columns[ 1 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 2 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 3 ].typeName ).toBe( "VARCHAR" ); - expect( columns[ 4 ].typeName ).toBe( "VARCHAR" ); + var columns = s.getQueryHelper().parseMetadata(GetMetaData( q )); + expect( columns[ 1 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 2 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 3 ].typeName ).toBe( VARCHAR_CHECK ); + expect( columns[ 4 ].typeName ).toBe( VARCHAR_CHECK ); }) }) diff --git a/test/suite.cfc b/test/suite.cfc index d9d2b6d..4e47082 100644 --- a/test/suite.cfc +++ b/test/suite.cfc @@ -7,6 +7,25 @@ component extends="testbox.system.BaseSpec"{ //Allow universal access including outside tests variables.s = newSpreadsheetInstance(); + + // These are differences between ACF/Lucee and Boxlang engine + variables.DOUBLE_CHECK = !s.getIsBoxlang() ? "DOUBLE" : "NUMERIC"; + variables.VARCHAR_CHECK = !s.getIsBoxlang() ? "VARCHAR" : "STRING"; + variables.TIME_WITH_MILLISECONDS_MASK = !s.getIsBoxlang() ? "hh:nn:ss:l" : "hh:nn:ss.L"; + + // BoxLang's QueryNew stores "" as null for VarChar columns even when explicitly passed. + // s.read() always returns "" for blank cells. Replace nulls with "" after building expected queries. + variables.normalizeExpectedQuery = ( required query q ) => { + if( !s.getIsBoxlang() ) + return q; + q.Each( ( row, rowNumber ) => { + row.Each( ( key, value ) => { + if( IsNull( value ) || !value.len()) + QuerySetCell( q, key, "", rowNumber ); + }); + }); + return q; + }; function beforeAll(){ s.flushOsgiBundle(); @@ -29,7 +48,7 @@ component extends="testbox.system.BaseSpec"{ } function afterAll(){ - WriteDump( var=s.getEnvironment(), label="Environment and settings" ); + if(!url.keyExists('reporter') || url.reporter != 'json') WriteDump( var=s.getEnvironment(), label="Environment and settings" ); if( FileExists( variables.tempXlsPath ) ) FileDelete( variables.tempXlsPath ); if( FileExists( variables.tempXlsxPath ) )