Skip to content

Commit 9ebe122

Browse files
committed
feat(extensions): add value-centric non-mutating materializers
1 parent 46d32c2 commit 9ebe122

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
2+
// This code is licensed under MIT license (see LICENSE.txt for details)
3+
using TemporalCollections.Collections;
4+
using TemporalCollections.Extensions;
5+
6+
namespace TemporalCollections.Tests.Collections
7+
{
8+
public class TemporalValueExtensionsTests
9+
{
10+
// Helper: creates a TemporalQueue<int> with values 1..n (timestamps auto-assigned, chronological == insertion order)
11+
private static TemporalQueue<int> MakeIntQueue(params int[] values)
12+
{
13+
var q = new TemporalQueue<int>();
14+
foreach (var v in values) q.Enqueue(v);
15+
return q;
16+
}
17+
18+
[Fact]
19+
public void ToValueList_ReturnsAllValuesInChronologicalOrder()
20+
{
21+
var source = MakeIntQueue(1, 2, 3, 4);
22+
23+
var list = source.ToValueList();
24+
25+
Assert.Equal(new[] { 1, 2, 3, 4 }, list);
26+
// Materialized: modifications to the result do not affect source
27+
list.Add(99);
28+
Assert.Equal(4, source.CountInRange(DateTime.MinValue, DateTime.MaxValue));
29+
}
30+
31+
[Fact]
32+
public void ToValueArray_ReturnsAllValuesInChronologicalOrder()
33+
{
34+
var source = MakeIntQueue(10, 20, 30);
35+
36+
var arr = source.ToValueArray();
37+
38+
Assert.Equal(new[] { 10, 20, 30 }, arr);
39+
}
40+
41+
[Fact]
42+
public void ToValueHashSet_ContainsUniqueValues_IgnoreOrder()
43+
{
44+
// Requires: public HashSet<T> ToValueHashSet<T>(..., IEqualityComparer<T>? comparer = null)
45+
var source = MakeIntQueue(1, 2, 2, 3, 3, 3);
46+
47+
var set = source.ToValueHashSet();
48+
49+
Assert.Equal(3, set.Count);
50+
Assert.True(set.SetEquals(new[] { 1, 2, 3 }));
51+
}
52+
53+
[Fact]
54+
public void ToValueDictionary_UsesKeySelector_LastWinsOnDuplicates()
55+
{
56+
// Requires: public Dictionary<TKey,T> ToValueDictionary<T,TKey>(..., Func<T,TKey> keySelector, IEqualityComparer<TKey>? comparer = null)
57+
// Key selector: parity (even/odd) so we force collisions.
58+
var source = MakeIntQueue(1, 2, 4, 6, 3, 8); // last even = 8, last odd = 3
59+
60+
var dict = source.ToValueDictionary(v => v % 2 == 0 ? "even" : "odd");
61+
62+
Assert.Equal(2, dict.Count);
63+
Assert.Equal(8, dict["even"]);
64+
Assert.Equal(3, dict["odd"]);
65+
}
66+
67+
[Fact]
68+
public void ToValueQueue_MaterializesFifoSequence()
69+
{
70+
// Requires: public Queue<T> ToValueQueue<T>(this ITimeQueryable<T> source)
71+
var source = MakeIntQueue(5, 6, 7);
72+
73+
var q = source.ToValueQueue();
74+
75+
Assert.Equal(new[] { 5, 6, 7 }, q.ToArray());
76+
// Dequeue should follow FIFO
77+
Assert.Equal(5, q.Dequeue());
78+
Assert.Equal(6, q.Dequeue());
79+
Assert.Equal(7, q.Dequeue());
80+
}
81+
82+
[Fact]
83+
public void ToValueStack_MaterializesLifoSequence()
84+
{
85+
// Requires: public Stack<T> ToValueStack<T>(this ITimeQueryable<T> source)
86+
var source = MakeIntQueue(1, 2, 3);
87+
88+
var s = source.ToValueStack();
89+
90+
// Stack enumerates from top to bottom (LIFO): 3,2,1
91+
Assert.Equal(new[] { 3, 2, 1 }, s.ToArray());
92+
Assert.Equal(3, s.Pop());
93+
Assert.Equal(2, s.Pop());
94+
Assert.Equal(1, s.Pop());
95+
}
96+
97+
[Fact]
98+
public void ToReadOnlyValueCollection_MaterializesAndIsReadOnly()
99+
{
100+
// Requires: public IReadOnlyCollection<T> ToReadOnlyValueCollection<T>(this ITimeQueryable<T> source)
101+
var source = MakeIntQueue(9, 8, 7);
102+
103+
var ro = source.ToReadOnlyValueCollection();
104+
105+
Assert.IsAssignableFrom<IReadOnlyCollection<int>>(ro);
106+
Assert.Equal(3, ro.Count);
107+
Assert.Contains(9, ro);
108+
Assert.Contains(8, ro);
109+
Assert.Contains(7, ro);
110+
111+
// Verify it is actually read-only (ReadOnlyCollection<T> implements ICollection<T> but Add throws)
112+
var asCollection = ro as ICollection<int>;
113+
Assert.NotNull(asCollection);
114+
Assert.True(asCollection!.IsReadOnly);
115+
Assert.Throws<NotSupportedException>(() => asCollection.Add(42));
116+
}
117+
}
118+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
2+
// This code is licensed under MIT license (see LICENSE.txt for details)
3+
using TemporalCollections.Abstractions;
4+
5+
namespace TemporalCollections.Extensions
6+
{
7+
/// <summary>
8+
/// Non-mutating, value-centric extensions for ITimeQueryable{T}.
9+
/// These helpers never re-insert elements, so original timestamps are preserved.
10+
/// </summary>
11+
public static class TemporalValueExtensions
12+
{
13+
/// <summary>
14+
/// Materializes all values (chronological order) into a List.
15+
/// </summary>
16+
/// <exception cref="ArgumentNullException">Thrown when source is null.</exception>
17+
public static List<T> ToValueList<T>(this ITimeQueryable<T> source)
18+
{
19+
ArgumentNullException.ThrowIfNull(source);
20+
21+
return source
22+
.GetInRange(DateTime.MinValue, DateTime.MaxValue)
23+
.Select(i => i.Value)
24+
.ToList();
25+
}
26+
27+
/// <summary>
28+
/// Materializes all values (chronological order) into an array.
29+
/// </summary>
30+
/// <exception cref="ArgumentNullException">Thrown when source is null.</exception>
31+
public static T[] ToValueArray<T>(this ITimeQueryable<T> source)
32+
{
33+
ArgumentNullException.ThrowIfNull(source);
34+
35+
return source
36+
.GetInRange(DateTime.MinValue, DateTime.MaxValue)
37+
.Select(i => i.Value)
38+
.ToArray();
39+
}
40+
41+
/// <summary>
42+
/// Materializes all values into a HashSet (chronological order is lost).
43+
/// </summary>
44+
public static HashSet<T> ToValueHashSet<T>(
45+
this ITimeQueryable<T> source,
46+
IEqualityComparer<T>? comparer = null)
47+
{
48+
ArgumentNullException.ThrowIfNull(source);
49+
50+
return comparer is null
51+
? source.GetInRange(DateTime.MinValue, DateTime.MaxValue)
52+
.Select(i => i.Value)
53+
.ToHashSet()
54+
: source.GetInRange(DateTime.MinValue, DateTime.MaxValue)
55+
.Select(i => i.Value)
56+
.ToHashSet(comparer);
57+
}
58+
59+
/// <summary>
60+
/// Materializes all values into a Dictionary using a key selector.
61+
/// Later duplicates override earlier ones.
62+
/// </summary>
63+
public static Dictionary<TKey, T> ToValueDictionary<T, TKey>(
64+
this ITimeQueryable<T> source,
65+
Func<T, TKey> keySelector,
66+
IEqualityComparer<TKey>? comparer = null)
67+
where TKey : notnull
68+
{
69+
ArgumentNullException.ThrowIfNull(source);
70+
ArgumentNullException.ThrowIfNull(keySelector);
71+
72+
var dict = comparer is null ? []
73+
: new Dictionary<TKey, T>(comparer);
74+
75+
foreach (var v in source.GetInRange(DateTime.MinValue, DateTime.MaxValue).Select(i => i.Value))
76+
{
77+
var key = keySelector(v);
78+
dict[key] = v; // last wins: overwrite any previous value for the same key
79+
}
80+
81+
return dict;
82+
}
83+
84+
/// <summary>
85+
/// Materializes all values into a Queue (FIFO, chronological order).
86+
/// </summary>
87+
public static Queue<T> ToValueQueue<T>(this ITimeQueryable<T> source)
88+
{
89+
ArgumentNullException.ThrowIfNull(source);
90+
91+
return new Queue<T>(
92+
source.GetInRange(DateTime.MinValue, DateTime.MaxValue).Select(i => i.Value));
93+
}
94+
95+
/// <summary>
96+
/// Materializes all values into a Stack (LIFO, last value on top).
97+
/// </summary>
98+
public static Stack<T> ToValueStack<T>(this ITimeQueryable<T> source)
99+
{
100+
ArgumentNullException.ThrowIfNull(source);
101+
102+
return new Stack<T>(
103+
source.GetInRange(DateTime.MinValue, DateTime.MaxValue).Select(i => i.Value));
104+
}
105+
106+
/// <summary>
107+
/// Materializes all values into a ReadOnlyCollection for safe exposure.
108+
/// </summary>
109+
public static IReadOnlyCollection<T> ToReadOnlyValueCollection<T>(this ITimeQueryable<T> source)
110+
{
111+
ArgumentNullException.ThrowIfNull(source);
112+
113+
return source.GetInRange(DateTime.MinValue, DateTime.MaxValue)
114+
.Select(i => i.Value)
115+
.ToList()
116+
.AsReadOnly();
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)