From 1586c5a04bd35dfb0532801bcde2b1e2ca714a8b Mon Sep 17 00:00:00 2001 From: HLWeil Date: Thu, 14 Mar 2024 19:44:19 +0100 Subject: [PATCH] fix json io datetime issues --- .gitignore | 2 +- src/FsSpreadsheet/Json/Cell.fs | 11 +++- src/FsSpreadsheet/Json/Row.fs | 10 ++- src/FsSpreadsheet/Json/Value.fs | 65 ++++++++++++++++--- src/FsSpreadsheet/Json/Worksheet.fs | 11 ++-- tests/FsSpreadsheet.Js.Tests/Json.Tests.fs | 3 - .../DefaultIO.Tests.fs | 2 - tests/FsSpreadsheet.Net.Tests/FsWorkbook.fs | 14 ++-- tests/FsSpreadsheet.Net.Tests/Json.Tests.fs | 5 +- tests/FsSpreadsheet.Net.Tests/Main.fs | 22 ++++++- tests/FsSpreadsheet.Net.Tests/OpenXml/Cell.fs | 9 +-- .../OpenXml/FsExtensions.fs | 11 ++-- .../FsSpreadsheet.Net.Tests/OpenXml/Sheet.fs | 9 +-- .../OpenXml/Spreadsheet.fs | 10 +-- .../OpenXml/Workbook.fs | 9 +-- .../Stylesheet.Tests.fs | 6 +- tests/FsSpreadsheet.Net.Tests/Table.fs | 4 +- .../ZipArchiveReader.fs | 13 ++-- tests/JS/Exceljs.js | 4 +- tests/TestUtils/DefaultTestObjects.fs | 4 +- 20 files changed, 147 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index 2f0773c..d9eedbc 100644 --- a/.gitignore +++ b/.gitignore @@ -357,7 +357,7 @@ output/ /tests/FsSpreadsheet.JsNativeTests/fable/**/*.js /tests/FsSpreadsheet.Net.Tests/TestFiles/WRITE_*.xlsx /tests/JS/TestFiles/WRITE_*.xlsx -/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.*.xlsx +/tests/TestUtils/TestFiles/TestWorkbook_FsSpreadsheet_WRITE.** /js **/py/** /.venv diff --git a/src/FsSpreadsheet/Json/Cell.fs b/src/FsSpreadsheet/Json/Cell.fs index b27a8a0..118aad9 100644 --- a/src/FsSpreadsheet/Json/Cell.fs +++ b/src/FsSpreadsheet/Json/Cell.fs @@ -3,16 +3,21 @@ open FsSpreadsheet open Thoth.Json.Core +[] +let column = "column" + [] let value = "value" let encode (cell:FsCell) = Encode.object [ + column, Encode.int cell.ColumnNumber value, Value.encode cell.Value ] -let decode : Decoder = +let decode rowNumber : Decoder = Decode.object (fun builder -> - let v = builder.Required.Field value (Value.decode) - new FsCell(v) + let v,dt = builder.Required.Field value (Value.decode) + let c = builder.Required.Field column Decode.int + new FsCell(v,dt,FsAddress(rowNumber,c)) ) diff --git a/src/FsSpreadsheet/Json/Row.fs b/src/FsSpreadsheet/Json/Row.fs index 0a32854..d633a42 100644 --- a/src/FsSpreadsheet/Json/Row.fs +++ b/src/FsSpreadsheet/Json/Row.fs @@ -6,12 +6,18 @@ open Thoth.Json.Core [] let cells = "cells" +[] +let number = "number" + let encode (row:FsRow) = Encode.object [ + number, Encode.int row.Index cells, Encode.seq (row.Cells |> Seq.map Cell.encode) ] -let decode : Decoder = +let decode : Decoder = Decode.object (fun builder -> - builder.Required.Field cells (Decode.seq Cell.decode) + let n = builder.Required.Field number Decode.int + let cs = builder.Required.Field cells (Decode.seq (Cell.decode n)) + n,cs ) \ No newline at end of file diff --git a/src/FsSpreadsheet/Json/Value.fs b/src/FsSpreadsheet/Json/Value.fs index f75e2be..6d48fda 100644 --- a/src/FsSpreadsheet/Json/Value.fs +++ b/src/FsSpreadsheet/Json/Value.fs @@ -1,21 +1,70 @@ module FsSpreadsheet.Json.Value open Thoth.Json.Core +open FsSpreadsheet + +#if FABLE_COMPILER_PYTHON +module PyTime = + + open Fable.Core + open Fable.Core.PyInterop + + // Currently in Fable, a created datetime object will contain a timezone. This is not allowed in python xlsx, so we need to remove it. + // Unfortunately, the timezone object in python is read-only, so we need to create a new datetime object without timezone. + // For this, we use the fromtimestamp method of the datetime module and convert the timestamp to a new datetime object without timezone. + + type DateTimeStatic = + [] + abstract member fromTimeStamp: timestamp:float -> System.DateTime + + [] + let DateTime : DateTimeStatic = nativeOnly + + let toUniversalTimePy (dt:System.DateTime) = + + dt.ToUniversalTime()?timestamp() + |> DateTime.fromTimeStamp +#endif + + + + + +module Decode = + + let datetime: Decoder = + #if FABLE_COMPILER_PYTHON + Decode.datetimeLocal |> Decode.map PyTime.toUniversalTimePy + #else + { new Decoder with + member _.Decode(helpers, value) = + if helpers.isString value then + match System.DateTime.TryParse(helpers.asString value) with + | true, datetime -> datetime |> Ok + | _ -> ("", BadPrimitive("a datetime", value)) |> Error + else + ("", BadPrimitive("a datetime", value)) |> Error + } + #endif let encode (value : obj) = match value with | :? string as s -> Encode.string s - | :? int as i -> Encode.int i | :? float as f -> Encode.float f + | :? int as i -> Encode.int i | :? bool as b -> Encode.bool b - | :? System.DateTime as d -> Encode.datetime d + | :? System.DateTime as d -> + d.ToString("O", System.Globalization.CultureInfo.InvariantCulture).Split('+').[0] + |> Encode.string | _ -> Encode.nil -let decode : Decoder = + + +let decode = Decode.oneOf [ - Decode.string |> Decode.map (fun s -> s :> obj) - Decode.int |> Decode.map (fun i -> i :> obj) - Decode.float |> Decode.map (fun f -> f :> obj) - Decode.bool |> Decode.map (fun b -> b :> obj) - Decode.datetimeLocal |> Decode.map (fun d -> d :> obj) + Decode.bool |> Decode.map (fun b -> b :> obj, DataType.Boolean) + Decode.int |> Decode.map (fun i -> i :> obj, DataType.Number) + Decode.float |> Decode.map (fun f -> f :> obj, DataType.Number) + Decode.datetime |> Decode.map (fun d -> d :> obj, DataType.Date) + Decode.string |> Decode.map (fun s -> s :> obj, DataType.String) ] \ No newline at end of file diff --git a/src/FsSpreadsheet/Json/Worksheet.fs b/src/FsSpreadsheet/Json/Worksheet.fs index f0e0b77..f126b2f 100644 --- a/src/FsSpreadsheet/Json/Worksheet.fs +++ b/src/FsSpreadsheet/Json/Worksheet.fs @@ -13,6 +13,7 @@ let rows = "rows" let tables = "tables" let encode (sheet:FsWorksheet) = + sheet.RescanRows() Encode.object [ name, Encode.string sheet.Name if Seq.isEmpty sheet.Tables |> not then @@ -28,11 +29,13 @@ let decode : Decoder = let rs = builder.Required.Field rows (Decode.seq Row.decode) let sheet = new FsWorksheet(n) rs - |> Seq.iteri (fun rowI cells -> - let r = sheet.Row(rowI + 1) + |> Seq.iter (fun (rowI,cells) -> + let r = sheet.Row(rowI) cells - |> Seq.iteri (fun coli cell -> - r[coli + 1].Value <- cell.Value + |> Seq.iter (fun cell -> + let c = r[cell.ColumnNumber] + c.Value <- cell.Value + c.DataType <- cell.DataType ) ) match ts with diff --git a/tests/FsSpreadsheet.Js.Tests/Json.Tests.fs b/tests/FsSpreadsheet.Js.Tests/Json.Tests.fs index 8ccf624..1316477 100644 --- a/tests/FsSpreadsheet.Js.Tests/Json.Tests.fs +++ b/tests/FsSpreadsheet.Js.Tests/Json.Tests.fs @@ -12,9 +12,6 @@ let defaultTestObject = let s = dto.ToJsonString() let dto2 = FsWorkbook.fromJsonString(s) TestingUtils.Expect.isDefaultTestObject dto2 - - testCase "Should Fail" <| fun _ -> - Expect.isTrue false "is not the expected DataType.Boolean" ] let main = testList "Json" [ diff --git a/tests/FsSpreadsheet.Net.Tests/DefaultIO.Tests.fs b/tests/FsSpreadsheet.Net.Tests/DefaultIO.Tests.fs index de46301..6912775 100644 --- a/tests/FsSpreadsheet.Net.Tests/DefaultIO.Tests.fs +++ b/tests/FsSpreadsheet.Net.Tests/DefaultIO.Tests.fs @@ -1,6 +1,5 @@ module DefaultIO -open Expecto open TestingUtils open FsSpreadsheet open FsSpreadsheet.Net @@ -45,7 +44,6 @@ let private tests_Write = testList "Write" [ ] -[] let main = testList "DefaultIO" [ tests_Read tests_Write diff --git a/tests/FsSpreadsheet.Net.Tests/FsWorkbook.fs b/tests/FsSpreadsheet.Net.Tests/FsWorkbook.fs index 2cec561..7518344 100644 --- a/tests/FsSpreadsheet.Net.Tests/FsWorkbook.fs +++ b/tests/FsSpreadsheet.Net.Tests/FsWorkbook.fs @@ -1,6 +1,4 @@ -module FsWorkbookTests - -open Expecto +module FsWorkbook.Tests open FsSpreadsheet open FsSpreadsheet.Net @@ -72,19 +70,17 @@ let performance = testList "Performace" [ testCase "ReadBigFile" (fun () -> let sw = Stopwatch() - let p = "./TestFiles/BigFile.xlsx" - sw.Start() - let wb = FsWorkbook.fromXlsxFile(p) + sw.Start() + let wb = FsWorkbook.fromXlsxFile(DefaultTestObject.BigFile.asRelativePath) sw.Stop() let elapsed = sw.Elapsed.Milliseconds - Expect.isLessThan elapsed 2000 $"Elapsed time should be less than 2000ms, but was {elapsed}ms" + Expect.isTrue (elapsed < 2000) $"Elapsed time should be less than 2000ms, but was {elapsed}ms" Expect.equal (wb.GetWorksheetAt(1).Rows.Count) 153991 "Row count should be 153991" ) ] -[] -let tests = +let main = testList "FsWorkbook" [ writeAndReadBytes performance diff --git a/tests/FsSpreadsheet.Net.Tests/Json.Tests.fs b/tests/FsSpreadsheet.Net.Tests/Json.Tests.fs index ea01380..28803b0 100644 --- a/tests/FsSpreadsheet.Net.Tests/Json.Tests.fs +++ b/tests/FsSpreadsheet.Net.Tests/Json.Tests.fs @@ -1,5 +1,6 @@ module Json.Tests +open TestingUtils open FsSpreadsheet open FsSpreadsheet.Net open Fable.Pyxpecto @@ -10,11 +11,9 @@ let defaultTestObject = testCase "Read-Write DefaultTestObject" <| fun _ -> let dto = DefaultTestObject.defaultTestObject() let s = dto.ToJsonString() + System.IO.File.WriteAllText(DefaultTestObject.FsSpreadsheetJSON.asRelativePath,s) let dto2 = FsWorkbook.fromJsonString(s) TestingUtils.Expect.isDefaultTestObject dto2 - - testCase "Should Fail" <| fun _ -> - Expect.isTrue false "is not the expected DataType.Boolean" ] let main = testList "Json" [ diff --git a/tests/FsSpreadsheet.Net.Tests/Main.fs b/tests/FsSpreadsheet.Net.Tests/Main.fs index d6a0bbf..e4314e2 100644 --- a/tests/FsSpreadsheet.Net.Tests/Main.fs +++ b/tests/FsSpreadsheet.Net.Tests/Main.fs @@ -1,6 +1,24 @@ module FsSpreadsheet.Net.Tests -open Expecto + +open Fable.Pyxpecto +open TestingUtils + +let all = + testList "All" + [ + ZipArchiveReader.main + Stylesheet.main + DefaultIO.main + FsExtension.Tests.main + Cell.Tests.main + Sheet.Tests.main + Workbook.Tests.main + Spreadsheet.Tests.main + Table.Tests.main + FsWorkbook.Tests.main + Json.Tests.main + ] [] let main argv = - Tests.runTestsInAssemblyWithCLIArgs [] argv \ No newline at end of file + Pyxpecto.runTests [||] all \ No newline at end of file diff --git a/tests/FsSpreadsheet.Net.Tests/OpenXml/Cell.fs b/tests/FsSpreadsheet.Net.Tests/OpenXml/Cell.fs index 3695bf1..d90c7fe 100644 --- a/tests/FsSpreadsheet.Net.Tests/OpenXml/Cell.fs +++ b/tests/FsSpreadsheet.Net.Tests/OpenXml/Cell.fs @@ -1,6 +1,6 @@ -module Cell +module Cell.Tests -open Expecto +open TestingUtils open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -17,7 +17,6 @@ let cbsi1Fox = wsp1Fox.Worksheet.Descendants() |> Array.ofSeq let nullCell = Cell.create (Some Spreadsheet.CellValues.Error) "A1" (Cell.CellValue.create "") nullCell.CellValue.Text <- null -[] let cellTests = testList "Cell" [ testList "includeSharedStringValue" [ @@ -28,4 +27,6 @@ let cellTests = testCase "nullCell with included SharedStringValue has no CellValueText" <| fun _ -> Expect.notEqual cissv1_0.CellValue.Text cissvNull.CellValue.Text "Does not differ" ] - ] \ No newline at end of file + ] + +let main = cellTests \ No newline at end of file diff --git a/tests/FsSpreadsheet.Net.Tests/OpenXml/FsExtensions.fs b/tests/FsSpreadsheet.Net.Tests/OpenXml/FsExtensions.fs index 3b7ea2e..94f7bfa 100644 --- a/tests/FsSpreadsheet.Net.Tests/OpenXml/FsExtensions.fs +++ b/tests/FsSpreadsheet.Net.Tests/OpenXml/FsExtensions.fs @@ -1,6 +1,6 @@ -module FsExtension +module FsExtension.Tests -open Expecto +open TestingUtils open FsSpreadsheet open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -50,7 +50,6 @@ dummyFsWorkbook.AddWorksheet(dummyFsWorksheet4) |> ignore let testFile2Path = Path.Combine(__SOURCE_DIRECTORY__, "../data", "2EXT02_Protein.xlsx") -[] let fsExtensionTests = testList "FsExtensions" [ //testList "DataType" [ @@ -163,7 +162,9 @@ let fsExtensionTests = Expect.equal v "Id" "value is not equal" testCase "Worksheet SwateTemplateMetadata from 2EXT02_Protein has FsRows" <| fun _ -> let rows = tf2Worksheet.Value.Rows - Expect.isGreaterThan rows.Count 0 "Worksheet SwateTemplateMetadata from 2EXT02_Protein has no FsRows" + Expect.notEqual rows.Count 0 "Worksheet SwateTemplateMetadata from 2EXT02_Protein has no FsRows" ] ] - ] \ No newline at end of file + ] + +let main = fsExtensionTests diff --git a/tests/FsSpreadsheet.Net.Tests/OpenXml/Sheet.fs b/tests/FsSpreadsheet.Net.Tests/OpenXml/Sheet.fs index 7993e60..c3a9a6b 100644 --- a/tests/FsSpreadsheet.Net.Tests/OpenXml/Sheet.fs +++ b/tests/FsSpreadsheet.Net.Tests/OpenXml/Sheet.fs @@ -1,6 +1,6 @@ -module Sheet +module Sheet.Tests -open Expecto +open TestingUtils open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -13,7 +13,6 @@ let shtsFox = wbFox.Sheets let shtssFox = shtsFox.Descendants() |> Array.ofSeq // array is needed since seqs cannot be compared -[] let sheetsTests = testList "Sheets" [ testList "get" [ @@ -26,4 +25,6 @@ let sheetsTests = let shtss = Sheet.Sheets.getSheets shtsFox |> Array.ofSeq // array is needed since seqs cannot be compared Expect.equal shtss shtssFox "Differs" ] - ] \ No newline at end of file + ] + +let main = sheetsTests \ No newline at end of file diff --git a/tests/FsSpreadsheet.Net.Tests/OpenXml/Spreadsheet.fs b/tests/FsSpreadsheet.Net.Tests/OpenXml/Spreadsheet.fs index ee38b81..3f72e91 100644 --- a/tests/FsSpreadsheet.Net.Tests/OpenXml/Spreadsheet.fs +++ b/tests/FsSpreadsheet.Net.Tests/OpenXml/Spreadsheet.fs @@ -1,6 +1,6 @@ -module Spreadsheet +module Spreadsheet.Tests -open Expecto +open TestingUtils open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -36,9 +36,6 @@ let cbsi1Fox = // get the Cells, but with their real values (inferred from //testSsdFox = testDoc //testSsdFox = testSsdFox2 - - -[] let spreadsheetTests = testList "Spreadsheet" [ let ssd = Spreadsheet.fromFile testFilePath false @@ -90,5 +87,4 @@ let spreadsheetTests = - -//testSsdFox.Close() \ No newline at end of file +let main = spreadsheetTests diff --git a/tests/FsSpreadsheet.Net.Tests/OpenXml/Workbook.fs b/tests/FsSpreadsheet.Net.Tests/OpenXml/Workbook.fs index 45a1fc9..fe2284b 100644 --- a/tests/FsSpreadsheet.Net.Tests/OpenXml/Workbook.fs +++ b/tests/FsSpreadsheet.Net.Tests/OpenXml/Workbook.fs @@ -1,6 +1,6 @@ -module Workbook +module Workbook.Tests -open Expecto +open TestingUtils open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -11,7 +11,6 @@ let wbpFox = ssdFox.WorkbookPart let wbFox = wbpFox.Workbook -[] let workbookTests = testList "Workbook" [ testList "get" [ @@ -19,4 +18,6 @@ let workbookTests = let wb = Workbook.get wbpFox Expect.equal wb wbFox "Differs" ] - ] \ No newline at end of file + ] + +let main = workbookTests \ No newline at end of file diff --git a/tests/FsSpreadsheet.Net.Tests/Stylesheet.Tests.fs b/tests/FsSpreadsheet.Net.Tests/Stylesheet.Tests.fs index 760f835..b59db0d 100644 --- a/tests/FsSpreadsheet.Net.Tests/Stylesheet.Tests.fs +++ b/tests/FsSpreadsheet.Net.Tests/Stylesheet.Tests.fs @@ -1,11 +1,8 @@ module Stylesheet -open Expecto -open FsSpreadsheet +open TestingUtils open FsSpreadsheet.Net open DocumentFormat.OpenXml.Spreadsheet -open DocumentFormat.OpenXml.Packaging -open DocumentFormat.OpenXml let private tests_NumberingFormat = testList "NumberingFormat" [ testList "isDateTime" [ @@ -29,7 +26,6 @@ let private tests_NumberingFormat = testList "NumberingFormat" [ ] ] -[] let main = testList "Stylesheet" [ tests_NumberingFormat ] \ No newline at end of file diff --git a/tests/FsSpreadsheet.Net.Tests/Table.fs b/tests/FsSpreadsheet.Net.Tests/Table.fs index c3ac9ee..f6b084f 100644 --- a/tests/FsSpreadsheet.Net.Tests/Table.fs +++ b/tests/FsSpreadsheet.Net.Tests/Table.fs @@ -1,6 +1,5 @@ -module FsTableTests +module Table.Tests -open Expecto open FsSpreadsheet open FsSpreadsheet.Net open DocumentFormat.OpenXml @@ -69,7 +68,6 @@ let transformTable = ] -[] let main = testList "FsTable" [ transformTable diff --git a/tests/FsSpreadsheet.Net.Tests/ZipArchiveReader.fs b/tests/FsSpreadsheet.Net.Tests/ZipArchiveReader.fs index 23eb78e..8bff1d9 100644 --- a/tests/FsSpreadsheet.Net.Tests/ZipArchiveReader.fs +++ b/tests/FsSpreadsheet.Net.Tests/ZipArchiveReader.fs @@ -7,9 +7,13 @@ open FsSpreadsheet.Net.ZipArchiveReader let tests_Read = testList "Read" [ let readFromTestFile (testFile: DefaultTestObject.TestFiles) = try - FsWorkbook.fromFile(testFile.asRelativePath) + let p = testFile.asRelativePath + FsWorkbook.fromFile(p) with - | _ -> FsWorkbook.fromFile($"{DefaultTestObject.testFolder}/{testFile.asFileName}") + | err -> + printfn "Could not read file from default path: %s" err.Message + let p = $"{DefaultTestObject.testFolder}/{testFile.asFileName}" + FsWorkbook.fromFile(p) testCase "FsCell equality" <| fun _ -> let c1 = FsCell(1, DataType.Number, FsAddress("A2")) @@ -38,14 +42,13 @@ open FsSpreadsheet.Net let performanceTest = testList "Performance" [ testCase "BigFile" <| fun _ -> - let readF() = FsWorkbook.fromFile("./TestFiles/BigFile.xlsx") |> ignore - let refReadF() = FsWorkbook.fromXlsxFile("./TestFiles/BigFile.xlsx") |> ignore + let readF() = FsWorkbook.fromFile(DefaultTestObject.BigFile.asRelativePath) |> ignore + let refReadF() = FsWorkbook.fromXlsxFile(DefaultTestObject.BigFile.asRelativePath) |> ignore Expect.isFasterThan readF refReadF "ZipArchiveReader should be faster than standard reader" //Expect.equal (wb.GetWorksheetAt(1).Rows.Count) 153991 "Row count should be equal" ] -[] let main = testList "ZipArchiveReader" [ performanceTest tests_Read diff --git a/tests/JS/Exceljs.js b/tests/JS/Exceljs.js index 5f0e033..2af82b5 100644 --- a/tests/JS/Exceljs.js +++ b/tests/JS/Exceljs.js @@ -97,7 +97,7 @@ describe('FsSpreadsheet.Js', function () { const table = new FsTable("MyNewTable", FsRangeAddress_$ctor_Z721C83C5("B1:D3")); fsws.AddTable(table); fsws.RescanRows() - await Xlsx.toFile(path, fswb) + await Xlsx.toXlsxFile(path, fswb) const readfswb = await Xlsx.fromXlsxFile(path) equal(readfswb.GetWorksheets().length, fswb.GetWorksheets().length) equal(readfswb.GetWorksheets()[0].Name, "My Awesome Worksheet") @@ -121,7 +121,7 @@ describe('FsSpreadsheet.Js', function () { equal(table.Name, "annotationTableStupidQuail41", "table.Name") equal(table.ShowHeaderRow, true, "table.ShowHeaderRow") // issue #69 const outoutPath = "tests/JS/TestFiles/WRITE_TestAssayExcel.xlsx" - await Xlsx.toFile(outoutPath, fswb) + await Xlsx.toXlsxFile(outoutPath, fswb) const fswb2 = await Xlsx.fromXlsxFile(outoutPath) equal(fswb2.GetWorksheets().length, 5) // test correct read }) diff --git a/tests/TestUtils/DefaultTestObjects.fs b/tests/TestUtils/DefaultTestObjects.fs index d76fbf2..9fd27e9 100644 --- a/tests/TestUtils/DefaultTestObjects.fs +++ b/tests/TestUtils/DefaultTestObjects.fs @@ -35,14 +35,16 @@ type WriteTestFiles = | FsSpreadsheetNET | FsSpreadsheetJS | FsSpreadsheetPY +| FsSpreadsheetJSON member this.asFileName = match this with + | FsSpreadsheetJSON -> "TestWorkbook_FsSpreadsheet_WRITE.json" | FsSpreadsheetNET -> "TestWorkbook_FsSpreadsheet_WRITE.net.xlsx" | FsSpreadsheetJS -> "TestWorkbook_FsSpreadsheet_WRITE.js.xlsx" | FsSpreadsheetPY -> "TestWorkbook_FsSpreadsheet_WRITE.py.xlsx" - member this.asRelativePath = $"{testFolder}/{this.asFileName}" + member this.asRelativePath = $"../TestUtils/{testFolder}/{this.asFileName}" member this.asRelativePathNode = $"./tests/TestUtils/{testFolder}/{this.asFileName}" module ExpectedRows =