Skip to content

Commit

Permalink
Merge pull request #425 from nfdi4plants/fixes
Browse files Browse the repository at this point in the history
Add  ArcTable cell accession and manipulation functions
  • Loading branch information
HLWeil authored Aug 7, 2024
2 parents dcfa9b5 + de4246f commit 6c267dd
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 38 deletions.
85 changes: 77 additions & 8 deletions src/Core/Table/ArcTable.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ARCtrl
namespace ARCtrl

open Fable.Core
open System.Collections.Generic
Expand Down Expand Up @@ -87,6 +87,16 @@ type ArcTable(name: string, headers: ResizeArray<CompositeHeader>, values: Syste
fun (table:ArcTable) ->
table.TryGetCellAt(column, row)

member this.GetCellAt (column: int,row: int) =
try
this.Values.[column,row]
with
| _ -> failwithf "Unable to find cell for index: (%i, %i) in table %s" column row this.Name

static member getCellAt (column: int,row: int) =
fun (table:ArcTable) ->
table.GetCellAt(column, row)

member this.IterColumns(action: CompositeColumn -> unit) =
for columnIndex in 0 .. (this.ColumnCount-1) do
let column = this.GetColumn columnIndex
Expand All @@ -110,19 +120,78 @@ type ArcTable(name: string, headers: ResizeArray<CompositeHeader>, values: Syste
copy

// - Cell API - //
// TODO: And then directly a design question. Is a column with rows containing both CompositeCell.Term and CompositeCell.Unitized allowed?
member this.UpdateCellAt(columnIndex, rowIndex,c : CompositeCell) =
SanityChecks.validateColumnIndex columnIndex this.ColumnCount false
SanityChecks.validateRowIndex rowIndex this.RowCount false
SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex],[|c|])
/// Update an already existing cell in the table. Fails if cell is outside the column AND row bounds of the table
member this.UpdateCellAt(columnIndex, rowIndex,c : CompositeCell, ?skipValidation) =
let skipValidation = defaultArg skipValidation false
if not(skipValidation) then
SanityChecks.validateColumnIndex columnIndex this.ColumnCount false
SanityChecks.validateRowIndex rowIndex this.RowCount false
c.ValidateAgainstHeader(this.Headers.[columnIndex],raiseException = true) |> ignore
Unchecked.setCellAt(columnIndex, rowIndex,c) this.Values

static member updateCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) =
/// Update an already existing cell in the table. Fails if cell is outside the columnd AND row bounds of the table
static member updateCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell, ?skipValidation) =
fun (table:ArcTable) ->
let newTable = table.Copy()
newTable.UpdateCellAt(columnIndex,rowIndex,cell)
newTable.UpdateCellAt(columnIndex,rowIndex,cell, ?skipValidation = skipValidation)
newTable


/// Update an already existing cell in the table, or adds a new cell if the row boundary is exceeded. Fails if cell is outside the column bounds of the table
member this.SetCellAt(columnIndex, rowIndex,c : CompositeCell, ?skipValidation) =
let skipValidation = defaultArg skipValidation false
if not(skipValidation) then
SanityChecks.validateColumnIndex columnIndex this.ColumnCount false
c.ValidateAgainstHeader(this.Headers.[columnIndex],raiseException = true) |> ignore
Unchecked.setCellAt(columnIndex, rowIndex,c) this.Values

/// Update an already existing cell in the table, or adds a new cell if the row boundary is exceeded. Fails if cell is outside the column bounds of the table
static member setCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell, ?skipValidation) =
fun (table:ArcTable) ->
let newTable = table.Copy()
newTable.SetCellAt(columnIndex,rowIndex,cell, ?skipValidation = skipValidation)
newTable


/// Update cells in a column by a function.
///
/// Inputs of the function are columnIndex, rowIndex, and the current cell.
member this.UpdateCellsBy(f : int -> int -> CompositeCell -> CompositeCell, ?skipValidation) =
let skipValidation = defaultArg skipValidation false
for kv in this.Values do
let ci,ri = kv.Key
let newCell = f ci ri kv.Value
if not(skipValidation) then newCell.ValidateAgainstHeader(this.Headers[ci],raiseException = true) |> ignore
Unchecked.setCellAt(ci, ri,newCell) this.Values

/// Update cells in a column by a function.
///
/// Inputs of the function are columnIndex, rowIndex, and the current cell.static member updateCellBy(f : int -> int -> CompositeCell -> CompositeCell) =
static member updateCellsBy(f : int -> int -> CompositeCell -> CompositeCell, ?skipValidation) =
fun (table:ArcTable) ->
let newTable = table.Copy()
newTable.UpdateCellsBy(f, ?skipValidation = skipValidation)

/// Update cells in a column by a function.
///
/// Inputs of the function are columnIndex, rowIndex, and the current cell.
member this.UpdateCellBy(columnIndex, rowIndex, f : CompositeCell -> CompositeCell, ?skipValidation) =
let skipValidation = defaultArg skipValidation false
if not(skipValidation) then
SanityChecks.validateColumnIndex columnIndex this.ColumnCount false
SanityChecks.validateRowIndex rowIndex this.RowCount false
let newCell = this.GetCellAt(columnIndex, rowIndex) |> f
if not(skipValidation) then newCell.ValidateAgainstHeader(this.Headers.[columnIndex],raiseException = true) |> ignore
Unchecked.setCellAt(columnIndex, rowIndex, newCell) this.Values

/// Update cells in a column by a function.
///
/// Inputs of the function are columnIndex, rowIndex, and the current cell.static member updateCellBy(f : int -> int -> CompositeCell -> CompositeCell) =
static member updateCellBy(columnIndex, rowIndex, f : CompositeCell -> CompositeCell, ?skipValidation) =
fun (table:ArcTable) ->
let newTable = table.Copy()
newTable.UpdateCellBy(columnIndex, rowIndex, f, ?skipValidation = skipValidation)

// - Header API - //
member this.UpdateHeader (index:int, newHeader: CompositeHeader, ?forceConvertCells: bool) =
let forceConvertCells = Option.defaultValue false forceConvertCells
Expand Down
23 changes: 23 additions & 0 deletions src/Core/Table/CompositeCell.fs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,29 @@ type CompositeCell =
| Unitized (v,oa) -> Unitized (v, oa.Copy())
| Data d -> Data (d.Copy())

member this.ValidateAgainstHeader(header : CompositeHeader, ?raiseException) =
let raiseExeption = Option.defaultValue false raiseException
let cell = this
match header, cell with
// no cell values will be handled later and is no error case
| isData when header.IsDataColumn && (cell.isData || cell.isFreeText) ->
true
| isData when header.IsDataColumn ->
if raiseExeption then
let msg = $"Invalid combination of header `{header}` and cell `{cell}`, Data header should have either Data or Freetext cells"
failwith msg
false
| isTerm when header.IsTermColumn && (cell.isTerm || cell.isUnitized) ->
true
| isNotTerm when (not header.IsTermColumn) && cell.isFreeText ->
true
| h, c ->
if raiseExeption then
let msg = $"Invalid combination of header `{h}` and cell `{cell}`"
failwith msg
// Maybe still return `msg` somehow if `raiseExeption` is false?
false

#if FABLE_COMPILER
//[<CompiledName("Term")>]
static member term (oa:OntologyAnnotation) = CompositeCell.Term(oa)
Expand Down
29 changes: 3 additions & 26 deletions src/Core/Table/CompositeColumn.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,9 @@ type CompositeColumn = {
/// ?raiseExeption: Default false. Set true if this function should raise an exception instead of return false.
// TODO! Do not only check cells.Head
member this.Validate(?raiseException: bool) =
let raiseExeption = Option.defaultValue false raiseException
let header = this.Header
let cells = this.Cells
match header, cells with
// no cell values will be handled later and is no error case
| _, emptyCell when cells.Length = 0 ->
true
| isData when header.IsDataColumn && (cells.[0].isData || cells.[0].isFreeText) ->
true
| isData when header.IsDataColumn ->
if raiseExeption then
let exampleCells = cells.[0]
let msg = $"Invalid combination of header `{header}` and cells `{exampleCells}`, Data header should have either Data or Freetext cells"
failwith msg
false
| isTerm when header.IsTermColumn && (cells.[0].isTerm || cells.[0].isUnitized) ->
true
| isNotTerm when (not header.IsTermColumn) && cells.[0].isFreeText ->
true
| h, c ->
if raiseExeption then
let exampleCells = c.[0]
let msg = $"Invalid combination of header `{h}` and cells `{exampleCells}`"
failwith msg
// Maybe still return `msg` somehow if `raiseExeption` is false?
false
this.Cells
|> Seq.exists (fun c -> c.ValidateAgainstHeader(this.Header, ?raiseException = raiseException) |> not)
|> not

/// <summary>
/// Returns an array of all units found in the cells of this column. Returns None if no units are found.
Expand Down
137 changes: 133 additions & 4 deletions tests/Core/ArcTable.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module ArcTable.Tests
module ArcTable.Tests

open ARCtrl

Expand Down Expand Up @@ -303,6 +303,63 @@ let private tests_ArcTableAux =
]
]

let private tests_GetCellAt =
testList "GetCellAt" [
testCase "InsideBoundaries" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])

Expect.equal (table.GetCellAt(0,0)) (CompositeCell.FreeText "Source_0") "Wrong at 0,0"
Expect.equal (table.GetCellAt(0,1)) (CompositeCell.FreeText "Source_1") "Wrong at 0,1"
Expect.equal (table.GetCellAt(1,0)) (CompositeCell.FreeText "Sample_0") "Wrong at 1,0"
Expect.equal (table.GetCellAt(1,1)) (CompositeCell.FreeText "Sample_1") "Wrong at 1,1"
)
testCase "Outside Boundaries" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])
let f = fun () -> table.GetCellAt(2,0) |> ignore

Expect.throws f "Should have failed"
)
]

let private tests_TryGetCellAt =
testList "TryGetCellAt" [
testCase "InsideBoundaries" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])

let cell1 = Expect.wantSome (table.TryGetCellAt(0,0)) "Should have found cell at 0,0"
let cell2 = Expect.wantSome (table.TryGetCellAt(0,1)) "Should have found cell at 0,1"
let cell3 = Expect.wantSome (table.TryGetCellAt(1,0)) "Should have found cell at 1,0"
let cell4 = Expect.wantSome (table.TryGetCellAt(1,1)) "Should have found cell at 1,1"

Expect.equal cell1 (CompositeCell.FreeText "Source_0") "Wrong at 0,0"
Expect.equal cell2 (CompositeCell.FreeText "Source_1") "Wrong at 0,1"
Expect.equal cell3 (CompositeCell.FreeText "Sample_0") "Wrong at 1,0"
Expect.equal cell4 (CompositeCell.FreeText "Sample_1") "Wrong at 1,1"

)
testCase "Outside Boundaries" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])
let cell = table.TryGetCellAt(2,0)
Expect.isNone None "Should have failed"
)



]


let private tests_UpdateHeader =
testList "UpdateHeader" [
testCase "ensure test table" (fun () ->
Expand Down Expand Up @@ -424,8 +481,8 @@ let private tests_UpdateHeader =
TestingUtils.Expect.sequenceEqual table.Values table2.Values "equal table values"
]

let private tests_UpdateCell =
testList "UpdateCell" [
let private tests_UpdateCellAt =
testList "UpdateCellAt" [
testCase "ensure test table" (fun () ->
let testTable = create_testTable()
Expect.equal testTable.RowCount 5 "RowCount"
Expand Down Expand Up @@ -473,6 +530,74 @@ let private tests_UpdateCell =
)
]

let private tests_SetCellAt =
testList "SetCellAt" [
testCase "Valid" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])
table.SetCellAt(0,1,CompositeCell.createFreeText "NewCell")

Expect.equal (table.GetCellAt(0,0)) (CompositeCell.FreeText "Source_0") "Wrong at 0,0"
Expect.equal (table.GetCellAt(0,1)) (CompositeCell.FreeText "NewCell") "Wrong at 0,1"
Expect.equal (table.GetCellAt(1,0)) (CompositeCell.FreeText "Sample_0") "Wrong at 1,0"
Expect.equal (table.GetCellAt(1,1)) (CompositeCell.FreeText "Sample_1") "Wrong at 1,1"
)
testCase "Outside Row boundary (Valid)" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])
table.SetCellAt(0,3,CompositeCell.createFreeText "NewCell")

Expect.equal (table.GetCellAt(0,0)) (CompositeCell.FreeText "Source_0") "Wrong at 0,0"
Expect.equal (table.GetCellAt(0,1)) (CompositeCell.FreeText "Source_1") "Wrong at 0,1"
Expect.equal (table.GetCellAt(1,0)) (CompositeCell.FreeText "Sample_0") "Wrong at 1,0"
Expect.equal (table.GetCellAt(1,1)) (CompositeCell.FreeText "Sample_1") "Wrong at 1,1"
Expect.equal (table.GetCellAt(0,3)) (CompositeCell.FreeText "NewCell") "Wrong at 0,3"
)
testCase "Outside column boundary (Invalid)" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])

let f = fun () ->table.SetCellAt(3,0,CompositeCell.createFreeText "NewCell")

Expect.throws f "Should have failed"
)
]

let private tests_UpdateCellsBy =
testList "UpdateCellsBy" [
testCase "InputAndOutput" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
let c2 = CompositeColumn.create(CompositeHeader.Output IOType.Sample, createCells_FreeText "Sample" 2)
table.AddColumns([|c1;c2|])
table.UpdateCellsBy(fun _ _ c ->
match c with
| CompositeCell.FreeText s -> CompositeCell.createFreeText (s + "_new")
| _ -> c
)
Expect.equal (table.GetCellAt(0,0)) (CompositeCell.FreeText "Source_0_new") "Wrong at 0,0"
Expect.equal (table.GetCellAt(0,1)) (CompositeCell.FreeText "Source_1_new") "Wrong at 0,1"
Expect.equal (table.GetCellAt(1,0)) (CompositeCell.FreeText "Sample_0_new") "Wrong at 1,0"
Expect.equal (table.GetCellAt(1,1)) (CompositeCell.FreeText "Sample_1_new") "Wrong at 1,1"
)
testCase "Fail For Wrong CellType" (fun () ->
let table = ArcTable.init "MyTable"
let c1 = CompositeColumn.create(CompositeHeader.Input IOType.Source, createCells_FreeText "Source" 2)
table.AddColumns([|c1|])
let f = fun () ->
table.UpdateCellsBy(fun _ _ c ->
c.ToTermCell()
)
Expect.throws f "Should have failed"
)
]

let private tests_UpdateColumn =
testList "UpdateColumn" [
testCase "ensure test table" (fun () ->
Expand Down Expand Up @@ -2403,8 +2528,12 @@ let main =
tests_SanityChecks
tests_ArcTableAux
tests_member
tests_GetCellAt
tests_TryGetCellAt
tests_UpdateHeader
tests_UpdateCell
tests_UpdateCellAt
tests_SetCellAt
tests_UpdateCellsBy
tests_UpdateColumn
tests_AddColumn_Mutable
tests_addColumn_Copy
Expand Down

0 comments on commit 6c267dd

Please sign in to comment.