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

Refactor functions and methods of OfficeInterop.fs #596

Merged
merged 3 commits into from
Jan 8, 2025
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
4 changes: 3 additions & 1 deletion src/Client/Client.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
<Compile Include="Api.fs" />
<Compile Include="Routing.fs" />
<Compile Include="OfficeInterop\InteropLogging.fs" />
<Compile Include="OfficeInterop\ExcelUtil.fs" />
<Compile Include="OfficeInterop\ARCtrlHelper.fs" />
<Compile Include="OfficeInterop\ExcelHelper.fs" />
<Compile Include="OfficeInterop\ArcTableHelper.fs" />
<Compile Include="OfficeInterop\OfficeInterop.fs" />
<Compile Include="States\DataAnnotator.fs" />
<Compile Include="States\ARCitect.fs" />
Expand Down
240 changes: 240 additions & 0 deletions src/Client/OfficeInterop/ARCtrlHelper.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
module OfficeInterop.ARCtrlHelper

open Fable.Core
open ExcelJS.Fable
open Excel

open Shared

open ARCtrl
open ARCtrl.Spreadsheet

/// <summary>
/// Check whether the selected column is a reference column or not
/// </summary>
/// <param name="name"></param>
let isReferenceColumn (name: string) =
ARCtrl.Spreadsheet.ArcTable.helperColumnStrings
|> Seq.exists (fun cName -> name.StartsWith cName)

/// <summary>
/// Group the columns to building blocks
/// </summary>
/// <param name="headers"></param>
let groupToBuildingBlocks (headers: string []) =
let ra: ResizeArray<ResizeArray<int*string>> = ResizeArray()
headers
|> Array.iteri (fun i header ->
if isReferenceColumn header then
ra.[ra.Count-1].Add(i, header)
else
ra.Add(ResizeArray([(i, header)]))
)
ra

let isTopLevelMetadataSheet (worksheetName: string) =
match worksheetName with
| name when
ArcAssay.isMetadataSheetName name
|| ArcInvestigation.isMetadataSheetName name
|| ArcStudy.isMetadataSheetName name
|| Template.isMetadataSheetName name -> true
| _ -> false

/// <summary>
/// Get the associated CompositeColumn for the given column index. Returns raw column names and indices associated to all compartments of the CompositeColumn.
/// </summary>
/// <param name="table"></param>
/// <param name="columnIndex"></param>
/// <param name="context"></param>
let getCompositeColumnInfoByIndex (table: Table) (columnIndex: float) (context: RequestContext) =
promise {
let headerRange = table.getHeaderRowRange()
let _ = headerRange.load(U2.Case2 (ResizeArray [|"columnIndex"; "values"; "columnCount"|])) |> ignore

return! context.sync().``then``(fun _ ->
let rebasedIndex = columnIndex - headerRange.columnIndex |> int
if rebasedIndex < 0 || rebasedIndex >= (int headerRange.columnCount) then
failwith "Cannot select building block outside of annotation table!"
let headers: string [] = [|for v in headerRange.values.[0] do v.Value :?> string|]
let selectedHeader = rebasedIndex, headers.[rebasedIndex]
let buildingBlockGroups = groupToBuildingBlocks headers
let selectedBuildingBlock =
buildingBlockGroups.Find(fun bb -> bb.Contains selectedHeader)
selectedBuildingBlock
)
}

/// <summary>
/// Checks whether the string seqs are part of a valid arc table or not and returns potential errors and index of them in the annotation table
/// </summary>
/// <param name="headers"></param>
/// <param name="rows"></param>
let validate (headers: #seq<string>) (rows: #seq<#seq<string>>) =

let columns =
Seq.append [headers] rows
|> Seq.transpose

let columnsArray =
columns
|> Seq.toArray
|> Array.map (Seq.toArray)

let groupedColumns = columnsArray |> ArcTable.groupColumnsByHeader

let indexedError =
groupedColumns
|> Array.mapi (fun i c ->
try
let _ = CompositeColumn.fromStringCellColumns c
None
with
| ex ->
//The target index must be adapted depending on the position of the error column
//because the columns were group based on potential main columns
let hasMainColumn =
groupedColumns.[i]
|> Array.map (fun gc ->
CompositeHeader.Cases
|> Array.exists (fun (_, header) -> gc.[0].StartsWith(header))
)
|> Array.contains true

if hasMainColumn then Some (ex, i)
else Some (ex, i - 1)
)
|> Array.filter (fun i -> i.IsSome)

let indexedError =
if indexedError.Length > 0 then indexedError |> Array.map (fun i -> i.Value)
else [||]

let newHeaders = headers |> Array.ofSeq

let errorIndices =
indexedError
|> Array.map (fun (ex, bi) ->
ex,
groupedColumns.[0..bi]
|> Array.map (fun bb -> bb.Length)
|> Array.sum
|> (fun i -> newHeaders.[i])
)

errorIndices

/// <summary>
/// Validate whether the selected excel table is a valid ARC / ISA table
/// </summary>
/// <param name="excelTable"></param>
/// <param name="context"></param>
let validateExcelTable (excelTable: Table) (context: RequestContext) =
promise {
//Get headers and body
let headerRange = excelTable.getHeaderRowRange()
let bodyRowRange = excelTable.getDataBodyRange()

let _ =
headerRange.load(U2.Case2 (ResizeArray [|"values"|])) |> ignore
bodyRowRange.load(U2.Case2 (ResizeArray [|"values"|])) |> ignore

do! context.sync()

let inMemoryTable =

let headers =
headerRange.values.[0]
|> Seq.map (fun item ->
item
|> Option.map string
|> Option.defaultValue ""
|> (fun s -> s.TrimEnd())
)

let bodyRows =
bodyRowRange.values
|> Seq.map (fun items ->
items
|> Seq.map (fun item ->
item
|> Option.map string
|> Option.defaultValue ""
)
)

validate headers bodyRows

return inMemoryTable
}

/// <summary>
/// Get output column of arc excel table
/// </summary>
/// <param name="context"></param>
let tryGetPrevTableOutput (prevArcTable: ArcTable option) =
promise {

if prevArcTable.IsSome then

let outputColumns = prevArcTable.Value.TryGetOutputColumn()

if(outputColumns.IsSome) then

let outputValues =
CompositeColumn.toStringCellColumns outputColumns.Value
|> (fun lists -> lists.Head.Head :: lists.Head.Tail)
|> Array.ofList

if outputValues.Length > 0 then return Some outputValues
else return None

else

return None

else
return None
}

/// <summary>
/// Prepare the given table to be joined with the currently active annotation table
/// </summary>
/// <param name="tableToAdd"></param>
let prepareTemplateInMemory (originTable: ArcTable) (tableToAdd: ArcTable) (selectedColumns:bool []) =
let selectedColumnIndices =
selectedColumns
|> Array.mapi (fun i item -> if item = false then Some i else None)
|> Array.choose (fun x -> x)
|> List.ofArray

let finalTable = Table.selectiveTablePrepare originTable tableToAdd selectedColumnIndices

finalTable

[<AutoOpen>]
module ARCtrlExtensions =

type ArcFiles with

/// <summary>
/// Output returns the expected sheetname and metadata values in string seqs form.
/// </summary>
member this.MetadataToExcelStringValues() =
match this with
| ArcFiles.Assay assay ->
let metadataWorksheetName = ArcAssay.metadataSheetName
let seqOfSeqs = ArcAssay.toMetadataCollection assay
metadataWorksheetName, seqOfSeqs
| ArcFiles.Investigation investigation ->
let metadataWorksheetName = ArcInvestigation.metadataSheetName
let seqOfSeqs = ArcInvestigation.toMetadataCollection investigation
metadataWorksheetName, seqOfSeqs
| ArcFiles.Study (study, assays) ->
let metadataWorksheetName = ArcStudy.metadataSheetName
let seqOfSeqs = ArcStudy.toMetadataCollection study (Option.whereNot List.isEmpty assays)
metadataWorksheetName, seqOfSeqs
| ArcFiles.Template template ->
let metadataWorksheetName = Template.metaDataSheetName
let seqOfSeqs = Template.toMetadataCollection template
metadataWorksheetName, seqOfSeqs
Loading
Loading