Skip to content

Commit a09813b

Browse files
committed
Add options to allow label customisation and easier filtering
Breaking: changes `CsvDefinition` from a `KeyValuePair<string, Func<T, string>>` to a custom `CsvField<T>` for simplicity to add more functionailty. - Adds option for overiding a label on `CsvDefinition` fields (key and label can now be different) - Adds option to provide an include function, to determine if the column should render or not (allows for simpler conditionally included columns)
1 parent 8a68084 commit a09813b

File tree

9 files changed

+313
-47
lines changed

9 files changed

+313
-47
lines changed

CSharpVitamins.Tabulation.Tests/CSharpVitamins.Tabulation.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
</Reference>
191191
</ItemGroup>
192192
<ItemGroup>
193+
<Compile Include="CsvFieldFacts.cs" />
193194
<Compile Include="CsvDefinitionFacts.cs" />
194195
<Compile Include="PlainTextTableFacts.cs" />
195196
<Compile Include="Properties\AssemblyInfo.cs" />
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using CSharpVitamins.Tabulation;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
using Xunit.Extensions;
11+
12+
//namespace CSharpVitamins.Tabulation.Tests
13+
namespace Tests
14+
{
15+
public class CsvFieldFacts
16+
{
17+
ITestOutputHelper output;
18+
19+
public CsvFieldFacts(ITestOutputHelper output)
20+
{
21+
this.output = output;
22+
}
23+
24+
class DemoModel
25+
{
26+
public string FieldA { get; set; }
27+
public string FieldB { get; set; }
28+
public string FieldC { get; set; }
29+
public string FieldD { get; set; }
30+
}
31+
32+
IList<DemoModel> create_data()
33+
{
34+
return new[]
35+
{
36+
new DemoModel { FieldA = "Row 1 A", FieldB = "B1", FieldC = "C1", FieldD = "D1" },
37+
new DemoModel { FieldA = "Row 2 A", FieldB = "B2", FieldC = "C2" },
38+
new DemoModel { FieldA = "Row 3 A" }
39+
};
40+
}
41+
42+
CsvDefinition<DemoModel> create_definition()
43+
{
44+
return new CsvDefinition<DemoModel>
45+
{
46+
{ "Field A", "Field A Label", item => item.FieldA },
47+
{ "Field B", item => item.FieldB },
48+
{ "Field C", item => item.FieldC, x => false },
49+
{ "Field D", "D Field", item => item.FieldD, x => true },
50+
{ "Field B", "", item => item.FieldB },
51+
{ "Field C", "Field C Again", item => item.FieldC },
52+
};
53+
}
54+
55+
[Fact]
56+
void observes_includedFields_and_labels()
57+
{
58+
var fields = create_definition();
59+
var data = create_data();
60+
61+
string result;
62+
using (var writer = new StringWriter())
63+
{
64+
fields.Write(writer, data, " | ");
65+
result = writer.ToString();
66+
}
67+
68+
const string expected = "Field A Label | Field B | D Field | | Field C Again"
69+
+ "\r\nRow 1 A | B1 | D1 | B1 | C1"
70+
+ "\r\nRow 2 A | B2 | | B2 | C2"
71+
+ "\r\nRow 3 A | | | | "
72+
+ "\r\n";
73+
74+
Assert.Equal(expected, result);
75+
}
76+
}
77+
}

CSharpVitamins.Tabulation/CSharpVitamins.Tabulation.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<ItemGroup>
4242
<Compile Include="Alignment.cs" />
4343
<Compile Include="ColumnState.cs" />
44+
<Compile Include="CsvField.cs" />
4445
<Compile Include="Divider.cs" />
4546
<Compile Include="CsvDefinitionFactory.cs" />
4647
<Compile Include="Properties\AssemblyInfo.cs" />

CSharpVitamins.Tabulation/CSharpVitamins.Tabulation.nuspec

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
`CharpVitamins.Tabulation` provides helpers to create tabular data with little fuss i.e. Tab or comma separated values (`CsvDefinition`),
1313
or padded columns of plain text (via `PlainTextTable`).</description>
1414
<releaseNotes>
15+
* 2.0.0: Breaking: changes `CsvDefinition` from a `KeyValuePair&lt;string, Func&lt;T, string&gt;&gt;` to a custom `CsvField&lt;T&gt;` for simplicity to add more functionailty.
16+
Adds option for overiding a label on `CsvDefinition` fields (key and label can now be different)
17+
Adds option to provide an include function, to determine if the column should render or not (allows for simpler conditionally included columns)
18+
1519
* 1.0.3: Now throws `ArgumentNullException` for non-nullable params to the `CsvDefinition.Write()` method
1620
* 1.0.2: Meta data update
1721
* 1.0.1: Adds `PlainTextTable.Divide()` method as a quick way to append a new divider.
@@ -20,4 +24,4 @@ or padded columns of plain text (via `PlainTextTable`).</description>
2024
<copyright>Copyright 2015</copyright>
2125
<tags>CSV Text PlainText</tags>
2226
</metadata>
23-
</package>
27+
</package>

CSharpVitamins.Tabulation/CsvDefinition.cs

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ namespace CSharpVitamins.Tabulation
99
/// Creates a definition of fields to be used as a tabular output
1010
/// </summary>
1111
/// <typeparam name="T"></typeparam>
12-
public class CsvDefinition<T> : List<KeyValuePair<string, Func<T, string>>>
12+
public class CsvDefinition<T> : List<CsvField<T>>
1313
{
14+
/// <summary />
1415
public CsvDefinition()
1516
: base()
1617
{ }
1718

19+
/// <summary />
1820
public CsvDefinition(int capacity)
1921
: base(capacity)
2022
{ }
2123

22-
public CsvDefinition(IEnumerable<KeyValuePair<string, Func<T, string>>> items)
24+
/// <summary />
25+
public CsvDefinition(IEnumerable<CsvField<T>> items)
2326
: base(items)
2427
{ }
2528

29+
// backwards compat
30+
/// <summary />
31+
public CsvDefinition(IEnumerable<KeyValuePair<string, Func<T, string>>> items)
32+
: base(items.Cast<CsvField<T>>())
33+
{ }
34+
2635
/// <summary>
2736
/// If the header has been output
2837
/// </summary>
@@ -44,17 +53,54 @@ public void Reset()
4453
/// <returns>True if the key was found.</returns>
4554
public bool Contains(string key, StringComparison comparison = StringComparison.Ordinal)
4655
{
47-
return this.FindIndex(x => string.Equals(x.Key, key, comparison)) != -1;
56+
return this.FindIndex(
57+
x => string.Equals(x.Key, key, comparison)
58+
) != -1;
4859
}
4960

5061
/// <summary>
5162
/// Adds a Key/Value pair to the definition - used for easy object initialisation
5263
/// </summary>
5364
/// <param name="key">The name of the column/header</param>
54-
/// <param name="value">The Func to convert T into a string for the column value</param>
55-
public void Add(string key, Func<T, string> value)
65+
/// <param name="picker">The Func to convert T into a string for the column value</param>
66+
public void Add(string key, Func<T, string> picker)
67+
{
68+
Add(new CsvField<T>(key, picker));
69+
}
70+
71+
/// <summary>
72+
///
73+
/// </summary>
74+
/// <param name="key"></param>
75+
/// <param name="label"></param>
76+
/// <param name="picker"></param>
77+
public void Add(string key, string label, Func<T, string> picker)
78+
{
79+
Add(new CsvField<T>(key, picker) { Label = label });
80+
}
81+
82+
/// <summary>
83+
///
84+
/// </summary>
85+
/// <param name="key"></param>
86+
/// <param name="label"></param>
87+
/// <param name="picker"></param>
88+
/// <param name="include"></param>
89+
public void Add(string key, Func<T, string> picker, Func<string, bool> include)
5690
{
57-
Add(new KeyValuePair<string, Func<T, string>>(key, value));
91+
Add(new CsvField<T>(key, picker) { Include = include, });
92+
}
93+
94+
/// <summary>
95+
///
96+
/// </summary>
97+
/// <param name="key"></param>
98+
/// <param name="label"></param>
99+
/// <param name="picker"></param>
100+
/// <param name="include"></param>
101+
public void Add(string key, string label, Func<T, string> picker, Func<string, bool> include)
102+
{
103+
Add(new CsvField<T>(key, picker) { Label = label, Include = include, });
58104
}
59105

60106
/// <summary>
@@ -65,7 +111,10 @@ public void Add(string key, Func<T, string> value)
65111
/// <returns>True if the field was found and removed, otherwise false</returns>
66112
public bool Remove(string key, StringComparison comparison = StringComparison.Ordinal)
67113
{
68-
var index = this.FindIndex(x => string.Equals(x.Key, key, comparison));
114+
var index = this.FindIndex(
115+
x => string.Equals(x.Key, key, comparison)
116+
);
117+
69118
if (index == -1)
70119
return false;
71120

@@ -81,37 +130,46 @@ public bool Remove(string key, StringComparison comparison = StringComparison.Or
81130
/// <param name="delimiter">The string to delimit column values with. A single character delimiter isare also used to escape the value, multi-character strings are not escaped.</param>
82131
public void Write(TextWriter writer, IEnumerable<T> rows, string delimiter = ",")
83132
{
84-
if (writer == null)
133+
if (null == writer)
85134
throw new ArgumentNullException(nameof(writer));
86135

87-
if (rows == null)
136+
if (null == rows)
88137
throw new ArgumentNullException(nameof(rows));
89138

90-
if (delimiter == null)
139+
if (null == delimiter)
91140
throw new ArgumentNullException(nameof(delimiter));
92141

93142
char[] escChars = delimiter.Length == 1
94143
? new[] { delimiter[0], '\n', '\r', '"' }
95144
: new[] { '\n', '\r', '"' };
96145

146+
var columns = this
147+
.Where(
148+
x => x.ShouldInclude
149+
)
150+
.ToArray();
151+
97152
if (!HeaderWritten)
98153
{
99154
HeaderWritten = true;
100155

101-
string header = string.Join(delimiter, this.Select(
102-
x => Escape(x.Key, escChars)
103-
));
156+
string header = string.Join(
157+
delimiter,
158+
columns.Select(
159+
x => Escape(x.Label ?? x.Key, escChars)
160+
)
161+
);
104162
writer.WriteLine(header);
105163
}
106164

107165
foreach (T row in rows)
108166
{
109167
string line = string.Join(
110168
delimiter,
111-
this.Select(
112-
x => Escape(x.Value(row), escChars) // x.Value == Func<T, string>
113-
));
114-
169+
columns.Select(
170+
x => Escape(x.PickValue(row), escChars)
171+
)
172+
);
115173
writer.WriteLine(line);
116174
}
117175
}
@@ -138,12 +196,26 @@ public PlainTextTable Tabulate(IEnumerable<T> rows, PlainTextTable tab = null)
138196
if (null == tab)
139197
tab = new PlainTextTable();
140198

141-
string[] line = this.Select(x => x.Key).ToArray();
199+
var columns = this
200+
.Where(
201+
x => x.ShouldInclude
202+
)
203+
.ToArray();
204+
205+
string[] line = columns
206+
.Select(
207+
x => x.Label ?? x.Key
208+
)
209+
.ToArray();
142210
tab.AddRow(line); // header
143211

144212
foreach (T row in rows)
145213
{
146-
line = this.Select(x => x.Value(row)).ToArray();
214+
line = columns
215+
.Select(
216+
x => x.PickValue(row)
217+
)
218+
.ToArray();
147219
tab.AddRow(line);
148220
}
149221

CSharpVitamins.Tabulation/CsvDefinitionFactory.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public class CsvDefinitionFactory
2222
public static Func<PropertyInfo, string> FailoverNameConverter =
2323
(prop) => prop.Name;
2424

25+
/// <summary />
26+
Func<PropertyInfo, bool> shouldInclude = prop => true;
27+
2528
/// <summary>
2629
///
2730
/// </summary>
@@ -43,6 +46,15 @@ public CsvDefinitionFactory()
4346
/// </summary>
4447
public IDictionary<Type, Func<PropertyInfo, object, string>> ValueConverters { get; set; }
4548

49+
/// <summary>
50+
/// Takes the details for a property and returns if the property should be included as a column (cannot be null)
51+
/// </summary>
52+
public Func<PropertyInfo, bool> ShouldInclude
53+
{
54+
get => shouldInclude;
55+
set => shouldInclude = value ?? throw new NullReferenceException($"{nameof(ShouldInclude)} cannot be set to `null`");
56+
}
57+
4658
/// <summary>
4759
/// Resolves the value converter for the type
4860
/// </summary>
@@ -61,24 +73,25 @@ static Func<PropertyInfo, object, string> resolve_converter(Type type, IDictiona
6173
/// <summary>
6274
/// Creates a definition from the given model's properties (prop.Name &amp; prop.Value.ToString())
6375
/// </summary>
64-
/// <typeparam name="T">The model to reflect on</typeparam>
76+
/// <typeparam name="Model">The model to reflect on</typeparam>
6577
/// <returns>A TableDefinition instance that can render rows of data given the model T</returns>
66-
public CsvDefinition<T> CreateFromModel<T>()
78+
public CsvDefinition<Model> CreateFromModel<Model>()
6779
{
6880
var nameOf = NameConverter ?? FailoverNameConverter;
6981

70-
return new CsvDefinition<T>(
71-
typeof(T)
82+
return new CsvDefinition<Model>(
83+
typeof(Model)
7284
.GetProperties()
73-
.Select(prop =>
85+
.Where(ShouldInclude)
86+
.Select(prop =>
7487
{
7588
var converter = resolve_converter(prop.PropertyType, this.ValueConverters);
7689

77-
return new KeyValuePair<string, Func<T, string>>(
90+
return new CsvField<Model>(
7891
nameOf(prop),
79-
x =>
92+
model =>
8093
{
81-
object value = prop.GetValue(x, null);
94+
object value = prop.GetValue(model, null);
8295
return converter(prop, value);
8396
}
8497
);

0 commit comments

Comments
 (0)