Skip to content

Instantly share code, notes, and snippets.

@SchlenkR
Last active September 14, 2021 08:56
Show Gist options
  • Save SchlenkR/1f5c76f40450017f2336f2a81a45fb84 to your computer and use it in GitHub Desktop.
Save SchlenkR/1f5c76f40450017f2336f2a81a45fb84 to your computer and use it in GitHub Desktop.
open System
open System.Reflection
open System.Collections
open System.Collections.Generic
// TODO: Allow linebreaks in cells for strings / sequences?
module Config =
// TODO: string trim/wrap
// TODO: alignment per data type
let mutable maxCellWidth = 50
let mutable maxRows = 100
let mutable maxTableWidth = try Console.BufferWidth with _ -> 250
let mutable dateTimeFormat = "G"
let mutable timeSpanFormat = "c"
let mutable rowCellSeparator = ": "
let mutable valueCellSeparator = " | "
type private Alignment = | Left | Right
type private Property = { name: string }
type FsiTable = FsiTable of string
let private tryGetElementType (input: obj) =
match input with
| :? IEnumerable as enumerable ->
// typeof<'a>
enumerable.GetType().GetInterfaces()
|> Array.tryFind (fun x ->
x.IsGenericType &&
x.GetGenericTypeDefinition() = typedefof<IEnumerable<_>>)
|> Option.map (fun x -> x.GetGenericArguments().[0])
| _ -> None
let private toTableString (properties: Property list) (input: IEnumerable<obj seq>) =
let truncateString (maxLength: int) (s: string) =
if s.Length < maxLength then s else s.Substring(0, maxLength)
let getColumnWidth (cells: string []) =
cells
|> Array.map (fun value -> value.Length)
|> Array.fold max 0
|> min Config.maxCellWidth
let headerRow = properties |> Seq.map (fun prop -> prop.name, Left) |> Seq.toList
let inputRows =
[ for row in input |> Seq.truncate Config.maxRows do
[ for cell in row do
match cell with
| :? string as v ->
v, Left
| :? float as v ->
v.ToString(fsi.FormatProvider), Right
| :? Int32 as v ->
v.ToString(fsi.FormatProvider), Right
| :? Int64 as v ->
v.ToString(fsi.FormatProvider), Right
| :? DateTime as v ->
v.ToString(Config.dateTimeFormat), Right
| :? TimeSpan as v ->
v.ToString(Config.timeSpanFormat), Right
// TODO: Special case formatting for sequences of prim. values
| null ->
"", Left
| v ->
// TODO: multi line cell values?
sprintf "%A" v |> fun s -> s.Replace("\r", "").Replace("\n", "\\n"), Left
]
]
let table =
[
yield headerRow
// TODO: alignment
yield! inputRows
]
|> Seq.mapi (fun i row ->
// prepend row index column
let rowIndexString = if i = 0 then "" else string (i - 1)
Seq.append [ rowIndexString, Left ] row
)
|> array2D
let columnWidths =
[
for coli in [0.. Array2D.length2 table - 1 ] do
yield getColumnWidth (table.[0.., coli] |> Array.map fst)
]
let renderedCells =
table |> Array2D.mapi (fun rowi coli (cellValue, alignment) ->
// TODO: alignment
let colWidth = columnWidths.[coli]
let truncatedCellValue = truncateString colWidth cellValue
let normedCellValue =
match alignment with
| Left -> sprintf "%-*s " colWidth truncatedCellValue
| Right -> sprintf "%*s " colWidth truncatedCellValue
let ws = " "
match rowi,coli with
| 0,0 -> $"{normedCellValue}{String.replicate Config.rowCellSeparator.Length ws}"
| _,0 -> $"{normedCellValue}{Config.rowCellSeparator}"
| _ -> $"{normedCellValue}{Config.valueCellSeparator}"
)
let rawRowStrings =
[
for rowi in [0.. Array2D.length1 renderedCells - 1 ] do
let rowValue = renderedCells.[rowi, 0..] |> String.concat ""
yield renderedCells.[rowi, 0..] |> String.concat ""
if rowi = 0 then
yield String.replicate rowValue.Length "-"
]
FsiTable(rawRowStrings |> String.concat "\n" |> sprintf "%s\n")
// TODO: break table apart when width exceeds max width
type Table =
static member toTable (input: IEnumerable<(string * string) []>) =
let properties =
input
|> Seq.tryHead
|> Option.map (Array.map fst)
|> Option.map (fun row ->
row
|> Seq.map (fun v -> { Property.name = v })
|> Seq.toList)
|> Option.defaultValue []
toTableString properties (input |> Seq.map (Array.map snd >> Seq.cast<obj>))
static member toTable (input: IEnumerable<'a>) =
let reflectionProps =
(tryGetElementType input).Value.GetProperties()
|> Seq.filter (fun p -> p.CanRead)
let properties =
reflectionProps
|> Seq.map (fun p -> { Property.name = p.Name })
|> Seq.toList
let enumerable =
seq {
for x in input |> Seq.cast<obj> do
seq {
for p in reflectionProps do
p.GetValue x
}
}
toTableString properties enumerable
module private FsiPrinter =
//let tablePrinterTransformer (x: obj) =
// tryGetElementType x
// |> Option.map (fun x -> Table.toTable x :> obj)
// |> Option.defaultValue null
let tablePrinter (x: FsiTable) =
let (FsiTable inner) = x
"\n\n" + inner + "\n\n"
let addFsiTablePrinter() =
//fsi.AddPrintTransformer FsiPrinter.tablePrinterTransformer
fsi.AddPrinter FsiPrinter.tablePrinter
module private Test =
type Address =
{ city: string
street: string
zipCode: int }
type Test1 =
{ name: string
degree: int
weight: float
birthDate: DateTime
address: Address
comment: string }
let address1 = { city = "Frankfurt"; street = "Hospitalstr."; zipCode = 60002 }
let address2 = { city = "NYC"; street = "Lex. Av."; zipCode = 43545 }
let input =
[
let defaultComment = "abcdefgh abcdefgh abcdefgh abcdefgh"
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
{ name = "Hans"; degree = 23; weight = 56.3; birthDate = DateTime(2000, 12, 15, 23, 45, 00); address = address1; comment = defaultComment }
{ name = "Jürgen"; degree = 12; weight = 114.1; birthDate = DateTime(1983, 2, 3, 14, 05, 00); address = address2; comment = defaultComment }
]
let result = Table.toTable input
//printfn "%s" Test.result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment