Created
September 4, 2022 05:21
-
-
Save usausa/0b3e8a975a6fcf3800c0ea8c0f080a8f to your computer and use it in GitHub Desktop.
SlidingPager
This file contains hidden or 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
public sealed class SlidingPager<T> : IList<T> | |
{ | |
private readonly List<T> source; | |
private readonly Func<T, DateTime> selector; | |
private int end; | |
private int start; | |
public bool IsReadOnly => true; | |
public int Count => start - end + 1; | |
public T this[int index] | |
{ | |
get => source[start - index]; | |
set => throw new NotSupportedException(); | |
} | |
public bool CanMove { get; } | |
public bool CanMoveNext { get; private set; } | |
public bool CanMovePrev { get; private set; } | |
public DateTime MinDateTime { get; private set; } | |
public DateTime MaxDateTime { get; private set; } | |
public SlidingPager(List<T> source, Func<T, DateTime> selector) | |
{ | |
// ソースは日付降順になっているデータへの実装 | |
this.source = source; | |
this.selector = selector; | |
if (source.Count > 0) | |
{ | |
var lastDateTime = selector(source[0]); | |
var firstDateTime = selector(source[^1]); | |
var limit = lastDateTime.AddDays(-1); | |
CanMove = lastDateTime - firstDateTime >= TimeSpan.FromDays(1); | |
end = 0; | |
if (CanMove) | |
{ | |
// 初期データ範囲は最後から最大で1日の範囲 | |
start = FindStart(1, limit); | |
MinDateTime = limit; | |
MaxDateTime = lastDateTime; | |
CanMovePrev = true; | |
} | |
else | |
{ | |
start = source.Count - 1; | |
MinDateTime = firstDateTime; | |
MaxDateTime = lastDateTime; | |
} | |
} | |
else | |
{ | |
start = -1; | |
} | |
} | |
public void MovePrev() | |
{ | |
if (!CanMovePrev) | |
{ | |
throw new InvalidOperationException(); | |
} | |
// 前日移動 | |
var minDateTime = MinDateTime.TimeOfDay != TimeSpan.Zero ? MinDateTime.Date : MinDateTime.AddDays(-1); | |
// 先頭に到達したら位置再補正 | |
var firstDateTime = selector(source[^1]); | |
if (minDateTime < firstDateTime) | |
{ | |
minDateTime = firstDateTime; | |
} | |
var maxDateTime = minDateTime.AddDays(1); | |
// 最初を先に計算、少なくとも今の最後は入りうる | |
start = FindStart(end, minDateTime); | |
// 最初は必ず存在するので最後も存在する | |
end = FindEnd(start, maxDateTime); | |
CanMovePrev = start < source.Count - 1 || selector(source[start]) > minDateTime; | |
CanMoveNext = true; | |
MinDateTime = minDateTime; | |
MaxDateTime = maxDateTime; | |
} | |
public void MoveNext() | |
{ | |
if (!CanMoveNext) | |
{ | |
throw new InvalidOperationException(); | |
} | |
// 翌日移動 | |
var maxDateTime = MaxDateTime.TimeOfDay != TimeSpan.Zero ? MaxDateTime.Date.AddDays(1) : MaxDateTime.AddDays(1); | |
// 最後に到達したら位置再補正 | |
var lastDateTime = selector(source[0]); | |
if (maxDateTime > lastDateTime) | |
{ | |
maxDateTime = lastDateTime; | |
} | |
var minDateTime = maxDateTime.AddDays(-1); | |
// 最後を先に計算、少なくとも今の最初は入りうる | |
end = FindEnd(start, maxDateTime); | |
// 最語は必ず存在するので最後も存在する | |
start = FindStart(end, minDateTime); | |
CanMovePrev = true; | |
CanMoveNext = end > 0 || selector(source[end]) < maxDateTime; | |
MinDateTime = minDateTime; | |
MaxDateTime = maxDateTime; | |
} | |
private int FindStart(int from, DateTime limit) | |
{ | |
for (var i = from + 1; i < source.Count; i++) | |
{ | |
if (selector(source[i]) <= limit) | |
{ | |
return i; | |
} | |
} | |
return from; | |
} | |
private int FindEnd(int from, DateTime limit) | |
{ | |
for (var i = from - 1; i >= 0; i--) | |
{ | |
if (selector(source[i]) >= limit) | |
{ | |
return i; | |
} | |
} | |
return from; | |
} | |
public IEnumerator<T> GetEnumerator() => throw new NotSupportedException(); | |
IEnumerator IEnumerable.GetEnumerator() => throw new NotSupportedException(); | |
public void Add(T item) => throw new NotSupportedException(); | |
public void Insert(int index, T item) => throw new NotSupportedException(); | |
public bool Remove(T item) => throw new NotSupportedException(); | |
public void RemoveAt(int index) => throw new NotSupportedException(); | |
public void Clear() => throw new NotSupportedException(); | |
public bool Contains(T item) => throw new NotSupportedException(); | |
public int IndexOf(T item) => throw new NotSupportedException(); | |
public void CopyTo(T[] array, int arrayIndex) => throw new NotSupportedException(); | |
} |
This file contains hidden or 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
public class SlidingPagerTest | |
{ | |
[Fact] | |
public void 単一日付で1日以内() | |
{ | |
var list = new List<Data> | |
{ | |
new() { Timestamp = DateTime.Parse("2022/09/03 03:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/03 01:00") } | |
}; | |
var pager = new SlidingPager<Data>(list, static x => x.Timestamp); | |
// Basic | |
Assert.False(pager.CanMove); | |
Assert.Equal(list[^1].Timestamp, pager.MinDateTime); | |
Assert.Equal(list[0].Timestamp, pager.MaxDateTime); | |
// Data | |
Assert.Equal(2, pager.Count); | |
Assert.Equal(list[^1], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
} | |
[Fact] | |
public void 暇たがりでで1日以内() | |
{ | |
var list = new List<Data> | |
{ | |
new() { Timestamp = DateTime.Parse("2022/09/03 03:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/02 21:00") } | |
}; | |
var pager = new SlidingPager<Data>(list, static x => x.Timestamp); | |
// Basic | |
Assert.False(pager.CanMove); | |
Assert.Equal(list[^1].Timestamp, pager.MinDateTime); | |
Assert.Equal(list[0].Timestamp, pager.MaxDateTime); | |
// Data | |
Assert.Equal(2, pager.Count); | |
Assert.Equal(list[^1], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
} | |
[Fact] | |
public void 日またがりで1日超過2点境界() | |
{ | |
var list = new List<Data> | |
{ | |
new() { Timestamp = DateTime.Parse("2022/09/03 05:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/02 05:00") } | |
}; | |
var pager = new SlidingPager<Data>(list, static x => x.Timestamp); | |
// Basic | |
Assert.True(pager.CanMove); | |
Assert.Equal(list[^1].Timestamp, pager.MinDateTime); | |
Assert.Equal(list[0].Timestamp, pager.MaxDateTime); | |
// Data | |
Assert.Equal(2, pager.Count); | |
Assert.Equal(list[^1], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Move | |
pager.MovePrev(); | |
// Condition | |
Assert.False(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Data | |
Assert.Equal(2, pager.Count); | |
Assert.Equal(list[^1], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
// Move | |
pager.MoveNext(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Data | |
Assert.Equal(2, pager.Count); | |
Assert.Equal(list[^1], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
} | |
[Fact] | |
public void 日またたがり3日() | |
{ | |
var list = new List<Data> | |
{ | |
new() { Timestamp = DateTime.Parse("2022/09/03 12:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/03 02:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/02 16:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/02 06:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/01 20:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/01 10:00") } | |
}; | |
var pager = new SlidingPager<Data>(list, static x => x.Timestamp); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 12:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 12:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(3 + 1, pager.Count); | |
Assert.Equal(list[3], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Move | |
pager.MovePrev(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 00:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 00:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(2 + 2, pager.Count); | |
Assert.Equal(list[4], pager[0]); | |
Assert.Equal(list[1], pager[^1]); | |
// Move | |
pager.MovePrev(); | |
// Condition | |
Assert.False(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/01 10:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/02 10:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(3 + 1, pager.Count); | |
Assert.Equal(list[5], pager[0]); | |
Assert.Equal(list[2], pager[^1]); | |
// Move | |
pager.MoveNext(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 00:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 00:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(2 + 2, pager.Count); | |
Assert.Equal(list[4], pager[0]); | |
Assert.Equal(list[1], pager[^1]); | |
// Move | |
pager.MoveNext(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 12:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 12:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(3 + 1, pager.Count); | |
Assert.Equal(list[3], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
} | |
[Fact] | |
public void 途中日に歯抜けデータ() | |
{ | |
var list = new List<Data> | |
{ | |
new() { Timestamp = DateTime.Parse("2022/09/03 12:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/03 02:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/01 20:00") }, | |
new() { Timestamp = DateTime.Parse("2022/09/01 10:00") } | |
}; | |
var pager = new SlidingPager<Data>(list, static x => x.Timestamp); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 12:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 12:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(2 + 1, pager.Count); | |
Assert.Equal(list[2], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Move | |
pager.MovePrev(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 00:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 00:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(0 + 2, pager.Count); | |
Assert.Equal(list[2], pager[0]); | |
Assert.Equal(list[1], pager[^1]); | |
// Move | |
pager.MovePrev(); | |
// Condition | |
Assert.False(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/01 10:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/02 10:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(2 + 1, pager.Count); | |
Assert.Equal(list[3], pager[0]); | |
Assert.Equal(list[1], pager[^1]); | |
// Move | |
pager.MoveNext(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.True(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 00:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 00:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(0 + 2, pager.Count); | |
Assert.Equal(list[2], pager[0]); | |
Assert.Equal(list[1], pager[^1]); | |
// Move | |
pager.MoveNext(); | |
// Condition | |
Assert.True(pager.CanMovePrev); | |
Assert.False(pager.CanMoveNext); | |
// Range | |
Assert.Equal(DateTime.Parse("2022/09/02 12:00"), pager.MinDateTime); | |
Assert.Equal(DateTime.Parse("2022/09/03 12:00"), pager.MaxDateTime); | |
// Data | |
Assert.Equal(2 + 1, pager.Count); | |
Assert.Equal(list[2], pager[0]); | |
Assert.Equal(list[0], pager[^1]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment