|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.Globalization;
namespace Microsoft.DotNet.Cli;
// Represents a table (with rows of type T) that can be printed to a terminal.
internal class PrintableTable<T>
{
public const string ColumnDelimiter = " ";
private readonly List<Column> _columns = [];
private class Column
{
public string Header { get; set; }
public Func<T, string> GetContent { get; set; }
public int MaxWidth { get; set; }
public override string ToString() { return Header; }
}
public void AddColumn(string header, Func<T, string> getContent, int maxWidth = int.MaxValue)
{
if (getContent == null)
{
throw new ArgumentNullException(nameof(getContent));
}
if (maxWidth <= 0)
{
throw new ArgumentException(
CliStrings.ColumnMaxWidthMustBeGreaterThanZero,
nameof(maxWidth));
}
_columns.Add(
new Column()
{
Header = header,
GetContent = getContent,
MaxWidth = maxWidth
});
}
public void PrintRows(IEnumerable<T> rows, Action<string> writeLine)
{
if (rows == null)
{
throw new ArgumentNullException(nameof(rows));
}
if (writeLine == null)
{
throw new ArgumentNullException(nameof(writeLine));
}
var widths = CalculateColumnWidths(rows);
var totalWidth = CalculateTotalWidth(widths);
if (totalWidth == 0)
{
return;
}
foreach (var line in EnumerateHeaderLines(widths))
{
writeLine(line);
}
writeLine(new string('-', totalWidth));
foreach (var row in rows)
{
foreach (var line in EnumerateRowLines(row, widths))
{
writeLine(line);
}
}
}
public int CalculateWidth(IEnumerable<T> rows)
{
if (rows == null)
{
throw new ArgumentNullException(nameof(rows));
}
return CalculateTotalWidth(CalculateColumnWidths(rows));
}
private IEnumerable<string> EnumerateHeaderLines(int[] widths)
{
if (_columns.Count != widths.Length)
{
throw new InvalidOperationException();
}
return EnumerateLines(widths, [.. _columns.Select(c => new StringInfo(c.Header ?? ""))]);
}
private IEnumerable<string> EnumerateRowLines(T row, int[] widths)
{
if (_columns.Count != widths.Length)
{
throw new InvalidOperationException();
}
return EnumerateLines(widths, [.. _columns.Select(c => new StringInfo(c.GetContent(row) ?? ""))]);
}
private static IEnumerable<string> EnumerateLines(int[] widths, StringInfo[] contents)
{
if (widths.Length != contents.Length)
{
throw new InvalidOperationException();
}
if (contents.Length == 0)
{
yield break;
}
var builder = new StringBuilder();
for (int line = 0; true; ++line)
{
builder.Clear();
bool emptyLine = true;
bool appendDelimiter = false;
for (int i = 0; i < contents.Length; ++i)
{
// Skip zero-width columns entirely
if (widths[i] == 0)
{
continue;
}
if (appendDelimiter)
{
builder.Append(ColumnDelimiter);
}
var startIndex = line * widths[i];
var length = contents[i].LengthInTextElements;
if (startIndex < length)
{
var endIndex = (line + 1) * widths[i];
length = endIndex >= length ? length - startIndex : widths[i];
builder.Append(contents[i].SubstringByTextElements(startIndex, length));
builder.Append(' ', widths[i] - length);
emptyLine = false;
}
else
{
// No more content for this column; append whitespace to fill remaining space
builder.Append(' ', widths[i]);
}
appendDelimiter = true;
}
if (emptyLine)
{
// Yield an "empty" line on the first line only
if (line == 0)
{
yield return builder.ToString();
}
yield break;
}
yield return builder.ToString();
}
}
private int[] CalculateColumnWidths(IEnumerable<T> rows)
{
return [.. _columns.Select(c =>
{
var width = new StringInfo(c.Header ?? "").LengthInTextElements;
foreach (var row in rows)
{
width = Math.Max(
width,
new StringInfo(c.GetContent(row) ?? "").LengthInTextElements);
}
return Math.Min(width, c.MaxWidth);
})];
}
private static int CalculateTotalWidth(int[] widths)
{
int sum = 0;
int count = 0;
foreach (var width in widths)
{
if (width == 0)
{
// Skip zero-width columns
continue;
}
sum += width;
++count;
}
if (count == 0)
{
return 0;
}
return sum + (ColumnDelimiter.Length * (count - 1));
}
}
|