Skip to content

Commit 058f615

Browse files
authored
Refactor SplitList (#509)
Class `Format`: * Explicitly limit the max. number of splits to `int.MaxValue` * `Format.Split(...)` now creates the `SplitList` cache, instead of the `SplitList` indexer Class `SplitList`: * Added new method `CreateSplitCache()` * `SplitList` indexer throws if `SplitList` cache is not filled Enhance unit test coverage for `SplitList`
1 parent f43120a commit 058f615

4 files changed

Lines changed: 190 additions & 71 deletions

File tree

src/SmartFormat.Tests/Core/ParserTests.cs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -306,37 +306,6 @@ public void Test_Format_IndexOf()
306306
});
307307
}
308308

309-
[Test]
310-
public void Test_Format_Split()
311-
{
312-
var parser = GetRegularParser();
313-
var format = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";
314-
var parsedFormat = parser.ParseFormat(format);
315-
var splits = parsedFormat.Split('|');
316-
317-
Assert.That(splits, Has.Count.EqualTo(3));
318-
Assert.Multiple(() =>
319-
{
320-
Assert.That(splits[0].ToString(), Is.EqualTo(" a"));
321-
Assert.That(splits[1].ToString(), Is.EqualTo("aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg"));
322-
Assert.That(splits[2].ToString(), Is.EqualTo("g "));
323-
});
324-
325-
// Test nested formats:
326-
var placeholder = (Placeholder) parsedFormat.Items[1];
327-
parsedFormat = placeholder.Format!;
328-
Assert.That(parsedFormat.ToString(), Is.EqualTo(" ccc dd|d {:|||} {eee} ff|f "));
329-
splits = parsedFormat.Split('|');
330-
331-
Assert.That(splits, Has.Count.EqualTo(3));
332-
Assert.Multiple(() =>
333-
{
334-
Assert.That(splits[0].ToString(), Is.EqualTo(" ccc dd"));
335-
Assert.That(splits[1].ToString(), Is.EqualTo("d {:|||} {eee} ff"));
336-
Assert.That(splits[2].ToString(), Is.EqualTo("f "));
337-
});
338-
}
339-
340309
[TestCase("{0:name:}", "name", "", "")]
341310
[TestCase("{0:name()}", "name", "", "")]
342311
[TestCase("{0:name(1|2|3)}", "name", "1|2|3", "")]
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using NUnit.Framework;
4+
using SmartFormat.Core.Parsing;
5+
using SmartFormat.Core.Settings;
6+
7+
namespace SmartFormat.Tests.Core.Parsing;
8+
9+
[TestFixture]
10+
public class SplitListTests
11+
{
12+
private static Parser GetRegularParser(SmartSettings? settings = null)
13+
{
14+
var parser = new Parser(settings ?? new SmartSettings
15+
{ StringFormatCompatibility = false, Parser = new ParserSettings { ErrorAction = ParseErrorAction.ThrowError } });
16+
return parser;
17+
}
18+
19+
[Test]
20+
public void Test_Format_Split()
21+
{
22+
var parser = GetRegularParser();
23+
const string format = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";
24+
var parsedFormat = parser.ParseFormat(format);
25+
var splits = parsedFormat.Split('|');
26+
27+
Assert.That(splits, Has.Count.EqualTo(3));
28+
Assert.Multiple(() =>
29+
{
30+
Assert.That(splits[0].ToString(), Is.EqualTo(" a"));
31+
// Split character inside nested format (Placeholder) is ignored:
32+
Assert.That(splits[1].ToString(), Is.EqualTo("aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg"));
33+
Assert.That(splits[2].ToString(), Is.EqualTo("g "));
34+
});
35+
}
36+
37+
[Test]
38+
public void SplitList_Should_EnumerateFormats()
39+
{
40+
var parser = GetRegularParser();
41+
// Pure literal with split characters
42+
const string format = "a|b|c";
43+
var parsedFormat = parser.ParseFormat(format);
44+
var splits = parsedFormat.Split('|');
45+
46+
// foreach is using GetEnumerator()
47+
var result1 = string.Empty;
48+
foreach (var split in splits)
49+
{
50+
result1 += split.ToString();
51+
}
52+
53+
// Use explicit interface for GetEnumerator
54+
var result2 = string.Empty;
55+
var enumerator = ((System.Collections.IEnumerable) splits).GetEnumerator();
56+
using var enumeratorDisposable = (IDisposable) enumerator;
57+
while (enumerator.MoveNext())
58+
{
59+
result2 += enumerator.Current!.ToString();
60+
}
61+
62+
Assert.Multiple(() =>
63+
{
64+
Assert.That(result1, Is.EqualTo("abc"));
65+
Assert.That(result2, Is.EqualTo("abc"));
66+
Assert.That(splits.IsReadOnly, Is.EqualTo(true));
67+
});
68+
}
69+
70+
[Test]
71+
public void CopyTo_ShouldCopyFormats()
72+
{
73+
var parser = GetRegularParser();
74+
// Pure literal with split characters
75+
const string format = "a|b|c";
76+
var parsedFormat = parser.ParseFormat(format);
77+
var splits = parsedFormat.Split('|');
78+
var array = new Format[3];
79+
splits.CopyTo(array, 0);
80+
81+
Assert.That(array, Is.EquivalentTo(splits));
82+
}
83+
84+
[Test]
85+
public void Indexer_ShouldThrow_IfCacheNotFilled()
86+
{
87+
var parser = GetRegularParser();
88+
// Pure literal with split characters
89+
const string format = "a|b";
90+
var parsedFormat = parser.ParseFormat(format);
91+
// Initialize SplitList directly without calling SplitList.CreateSplitCache()
92+
var splits = new SplitList().Initialize(parsedFormat, [format.IndexOf('|')]);
93+
94+
Assert.That(() => _ = splits[0], Throws.Exception.InstanceOf<InvalidOperationException>());
95+
}
96+
97+
[Test]
98+
public void InvalidIndexCall_ShouldThrow()
99+
{
100+
var parser = GetRegularParser();
101+
// Pure literal with split characters
102+
const string format = "a|b|c";
103+
var parsedFormat = parser.ParseFormat(format);
104+
var splits = parsedFormat.Split('|');
105+
106+
Assert.That(() => _ = splits[int.MaxValue], Throws.Exception.InstanceOf<ArgumentOutOfRangeException>());
107+
}
108+
109+
[Test]
110+
public void UnsupportedMemberCalls_ShouldThrow()
111+
{
112+
// Track although SplitList is internal
113+
114+
var parser = GetRegularParser();
115+
// Pure literal with split characters
116+
const string format = "a|b|c";
117+
var fmt = parser.ParseFormat(format);
118+
var splits = fmt.Split('|');
119+
120+
Assert.Multiple(() =>
121+
{
122+
Assert.That(() => splits[0] = fmt, Throws.Exception.InstanceOf<NotSupportedException>());
123+
Assert.That(() => splits.IndexOf(fmt), Throws.Exception.InstanceOf<NotSupportedException>());
124+
Assert.That(() => splits.Insert(0, fmt), Throws.Exception.InstanceOf<NotSupportedException>());
125+
Assert.That(() => splits.Add(fmt), Throws.Exception.InstanceOf<NotSupportedException>());
126+
Assert.That(() => splits.Contains(fmt), Throws.Exception.InstanceOf<NotSupportedException>());
127+
Assert.That(() => splits.Remove(fmt), Throws.Exception.InstanceOf<NotSupportedException>());
128+
Assert.That(() => splits.RemoveAt(0), Throws.Exception.InstanceOf<NotSupportedException>());
129+
});
130+
}
131+
}

src/SmartFormat/Core/Parsing/Format.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,16 @@ private List<int> FindAll(char search, int maxCount)
287287

288288
/// <summary>
289289
/// Splits the <see cref="Format"/> items by the given search character.
290+
/// The number of splits is limited to <see cref="int.MaxValue"/>.
290291
/// </summary>
291292
/// <param name="search">The search character used to split.</param>
292-
/// <returns></returns>
293+
/// <returns>An <see cref="IList{T}"/> of <see cref="Format"/>s.</returns>
293294
public IList<Format> Split(char search)
294295
{
295296
if (_splitCache == null || _splitCacheChar != search)
296297
{
297298
_splitCacheChar = search;
298-
_splitCache = Split(search, -1);
299+
_splitCache = Split(search, int.MaxValue);
299300
}
300301

301302
return _splitCache;
@@ -309,8 +310,11 @@ public IList<Format> Split(char search)
309310
/// <returns>An <see cref="IList{T}"/> of <see cref="Format"/>s.</returns>
310311
public IList<Format> Split(char search, int maxCount)
311312
{
313+
maxCount = Math.Abs(maxCount);
312314
var splits = FindAll(search, maxCount);
315+
// Initialize the SplitList and prefill the cache:
313316
var splitList = SplitListPool.Instance.Get().Initialize(this, splits);
317+
splitList.CreateSplitCache();
314318

315319
// Keep track of the split lists we create,
316320
// so that they can be returned to the object pool for later reuse.

src/SmartFormat/Core/Parsing/SplitList.cs

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class SplitList : IList<Format>
2020

2121
private Format _format = InitializationObject.Format;
2222
private List<int> _splits = InitializationObject.IntegerList;
23-
private readonly List<Format?> _formatCache = new();
23+
private readonly List<Format?> _splitFormatCache = new();
2424

2525
/// <summary>
2626
/// CTOR for object pooling.
@@ -42,9 +42,10 @@ public SplitList Initialize(Format format, List<int> splits)
4242
_format = format;
4343
_splits = splits;
4444

45-
// Resize the cache to match
45+
// Pre-size the cache to match
46+
// If we have N splits, we have N+1 resulting formats (see Count property)
4647
for (var i = 0; i < Count; ++i)
47-
_formatCache.Add(null);
48+
_splitFormatCache.Add(null);
4849

4950
return this;
5051
}
@@ -63,31 +64,44 @@ public Format this[int index]
6364

6465
if (_splits.Count == 0) return _format;
6566

66-
// Return the cached version?
67-
if (_formatCache[index] != null)
68-
return _formatCache[index]!;
67+
// The cache was initialized with nulls, but not yet filled
68+
if (_splitFormatCache.Exists(c => c is null))
69+
throw new InvalidOperationException("SplitList cache was not filled.");
6970

71+
// Return the result from the cache
72+
// which was created in method Format.Split(...)
73+
return _splitFormatCache[index]!;
74+
}
75+
set => throw new NotSupportedException();
76+
}
77+
78+
internal void CreateSplitCache()
79+
{
80+
if (_splits.Count == 0) return;
81+
82+
// If we have N splits, we have N+1 resulting formats
83+
for (var index = 0; index <= _splits.Count; index++)
84+
{
85+
Format f;
7086
if (index == 0)
7187
{
72-
var f = _format.Substring(0, _splits[0]);
73-
_formatCache[index] = f;
74-
return f;
88+
f = _format.Substring(0, _splits[0]);
89+
_splitFormatCache[index] = f;
90+
continue;
7591
}
7692

7793
if (index == _splits.Count)
7894
{
79-
var f = _format.Substring(_splits[index - 1] + 1);
80-
_formatCache[index] = f;
81-
return f;
95+
f = _format.Substring(_splits[index - 1] + 1);
96+
_splitFormatCache[index] = f;
97+
continue;
8298
}
8399

84-
// Return the format between the splits
100+
// The format between the splits
85101
var startIndex = _splits[index - 1] + 1;
86-
var format = _format.Substring(startIndex, _splits[index] - startIndex);
87-
_formatCache[index] = format;
88-
return format;
102+
f = _format.Substring(startIndex, _splits[index] - startIndex);
103+
_splitFormatCache[index] = f;
89104
}
90-
set => throw new NotSupportedException();
91105
}
92106

93107
/// <summary>
@@ -101,11 +115,13 @@ public void Clear()
101115
_splits = InitializationObject.IntegerList;
102116

103117
// Return the Formats we created to the pool
104-
for (var i = 0; i < _formatCache.Count; i++)
105-
if (_formatCache[i] != null)
106-
FormatPool.Instance.Return(_formatCache[i]!);
107-
108-
_formatCache.Clear();
118+
for (var i = 0; i < _splitFormatCache.Count; i++)
119+
{
120+
if (_splitFormatCache[i] != null)
121+
FormatPool.Instance.Return(_splitFormatCache[i]!);
122+
}
123+
124+
_splitFormatCache.Clear();
109125
}
110126

111127
///<inheritdoc/>
@@ -121,6 +137,21 @@ public void CopyTo(Format[] array, int arrayIndex)
121137
///<inheritdoc/>
122138
public bool IsReadOnly => true;
123139

140+
///<inheritdoc/>
141+
public IEnumerator<Format> GetEnumerator()
142+
{
143+
for (var i = 0; i < Count; i++)
144+
{
145+
yield return this[i];
146+
}
147+
}
148+
149+
///<inheritdoc/>
150+
IEnumerator IEnumerable.GetEnumerator()
151+
{
152+
return GetEnumerator();
153+
}
154+
124155
#endregion
125156

126157
#region: NotSupported IList Interface :
@@ -173,21 +204,5 @@ public bool Remove(Format item)
173204
throw new NotSupportedException();
174205
}
175206

176-
/// <summary>
177-
/// This method is not implemented.
178-
/// </summary>
179-
public IEnumerator<Format> GetEnumerator()
180-
{
181-
throw new NotSupportedException();
182-
}
183-
184-
/// <summary>
185-
/// This method is not implemented.
186-
/// </summary>
187-
IEnumerator IEnumerable.GetEnumerator()
188-
{
189-
throw new NotSupportedException();
190-
}
191-
192207
#endregion
193208
}

0 commit comments

Comments
 (0)