Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ArcTable cell accession and manipulation functions #425

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading