Skip to content

Instantly share code, notes, and snippets.

@usausa
Created September 4, 2022 05:21
Show Gist options
  • Save usausa/0b3e8a975a6fcf3800c0ea8c0f080a8f to your computer and use it in GitHub Desktop.
Save usausa/0b3e8a975a6fcf3800c0ea8c0f080a8f to your computer and use it in GitHub Desktop.
SlidingPager
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();
}
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