Last active
September 14, 2021 08:56
-
-
Save SchlenkR/1f5c76f40450017f2336f2a81a45fb84 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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