From 28ead8d25b8c04db51eb96996cada195f5126b87 Mon Sep 17 00:00:00 2001 From: gcastellov Date: Wed, 27 Aug 2025 17:16:10 +0200 Subject: [PATCH 1/3] eta & remaining time --- src/Progress.Samples.Bar.App/Program.cs | 2 ++ .../Program.cs | 2 ++ src/Progress.Samples.Pulse.App/Program.cs | 2 ++ src/Progress.Samples.Spinner.App/Program.cs | 2 ++ src/Progress/Components/Bar.cs | 7 ++-- src/Progress/Components/Component.cs | 8 +++-- src/Progress/Components/HearthBeat.cs | 7 ++-- src/Progress/Components/Pulse.cs | 7 ++-- src/Progress/Components/Spinner.cs | 6 ++-- src/Progress/Progress.csproj | 2 +- src/Progress/Reporter.cs | 34 +++++++++++++++---- src/Progress/ReporterBuilder.cs | 24 +++++++++++++ src/Progress/Timer.cs | 30 ++++++++++++++++ 13 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/Progress/Timer.cs diff --git a/src/Progress.Samples.Bar.App/Program.cs b/src/Progress.Samples.Bar.App/Program.cs index 4125db7..e575b27 100644 --- a/src/Progress.Samples.Bar.App/Program.cs +++ b/src/Progress.Samples.Bar.App/Program.cs @@ -5,6 +5,8 @@ using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) diff --git a/src/Progress.Samples.HearthBeat.App/Program.cs b/src/Progress.Samples.HearthBeat.App/Program.cs index 6acbdfa..21f50e8 100644 --- a/src/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Progress.Samples.HearthBeat.App/Program.cs @@ -5,6 +5,8 @@ using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) diff --git a/src/Progress.Samples.Pulse.App/Program.cs b/src/Progress.Samples.Pulse.App/Program.cs index ed89083..7757123 100644 --- a/src/Progress.Samples.Pulse.App/Program.cs +++ b/src/Progress.Samples.Pulse.App/Program.cs @@ -5,6 +5,8 @@ using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) diff --git a/src/Progress.Samples.Spinner.App/Program.cs b/src/Progress.Samples.Spinner.App/Program.cs index 8ed751e..f689c62 100644 --- a/src/Progress.Samples.Spinner.App/Program.cs +++ b/src/Progress.Samples.Spinner.App/Program.cs @@ -5,6 +5,8 @@ using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) diff --git a/src/Progress/Components/Bar.cs b/src/Progress/Components/Bar.cs index 0dfc4aa..282e89f 100644 --- a/src/Progress/Components/Bar.cs +++ b/src/Progress/Components/Bar.cs @@ -8,7 +8,6 @@ internal class Bar : Component private readonly uint _width; private readonly char[] _bar; - private Percent _percent = default!; private ulong _availableItems; private ulong _count; private int _adjustedPercent; @@ -26,7 +25,7 @@ public override Component Next(ulong availableItems, ulong currentCount) { _availableItems = availableItems; _count = currentCount; - _percent = Calculate(availableItems, currentCount); + CurrentPercent = Calculate(availableItems, currentCount); return this; } @@ -45,7 +44,7 @@ public override string ToString() if (DisplayPercent) { sBuilder.Append(' '); - sBuilder.Append(_percent.ToString()); + sBuilder.Append(CurrentPercent.ToString()); } return sBuilder.ToString(); @@ -53,7 +52,7 @@ public override string ToString() private void Fill() { - if (_percent.Value > 0) + if (CurrentPercent.Value > 0) _adjustedPercent = (int)(_count / (decimal)_availableItems * _width); if (_adjustedPercent <= _bar.Length) diff --git a/src/Progress/Components/Component.cs b/src/Progress/Components/Component.cs index 7f7ab96..0182df2 100644 --- a/src/Progress/Components/Component.cs +++ b/src/Progress/Components/Component.cs @@ -6,21 +6,23 @@ internal abstract class Component { private static readonly CultureInfo Culture = new("en-US"); + public Percent CurrentPercent { get; protected set; } = default!; + public abstract Component Next(ulong availableItems, ulong currentCount); protected static Percent Calculate(ulong availableItems, ulong currentCount) => new(availableItems, currentCount); internal class Percent { - private readonly decimal _percent; + private readonly double _percent; public Percent(ulong items, ulong count) { if (count > 0) - _percent = count / (decimal)items; + _percent = count / (double)items; } - public decimal Value => _percent * 100; + public double Value => _percent * 100; public override string ToString() => _percent.ToString("P02", Culture); } diff --git a/src/Progress/Components/HearthBeat.cs b/src/Progress/Components/HearthBeat.cs index 229e3ba..d78b51b 100644 --- a/src/Progress/Components/HearthBeat.cs +++ b/src/Progress/Components/HearthBeat.cs @@ -9,7 +9,6 @@ internal class HearthBeat : Component private uint _leftIndex; private uint _rightIndex; - private Percent _percent = default!; public HearthBeat(uint width, char progressSymbol) { @@ -21,7 +20,7 @@ public HearthBeat(uint width, char progressSymbol) public override Component Next(ulong availableItems, ulong currentCount) { - _percent = Calculate(availableItems, currentCount); + CurrentPercent = Calculate(availableItems, currentCount); if (_leftIndex == 0) { @@ -53,7 +52,7 @@ public override string ToString() if (DisplayPercent) { sBuilder.Append(' '); - sBuilder.Append(_percent.ToString()); + sBuilder.Append(CurrentPercent.ToString()); } return sBuilder.ToString(); @@ -74,7 +73,7 @@ private void Fill() { Array.Fill(_bar, ' '); - if (_percent.Value < 100) + if (CurrentPercent.Value < 100) { _bar[_leftIndex] = _progressSymbol; _bar[_rightIndex] = _progressSymbol; diff --git a/src/Progress/Components/Pulse.cs b/src/Progress/Components/Pulse.cs index 5602878..ba2f6b2 100644 --- a/src/Progress/Components/Pulse.cs +++ b/src/Progress/Components/Pulse.cs @@ -8,7 +8,6 @@ internal class Pulse : Component private readonly char[] _bar; private uint _index; - private Percent _percent = default!; public Pulse(uint width, char progressSymbol) { @@ -21,7 +20,7 @@ public Pulse(uint width, char progressSymbol) public override Component Next(ulong availableItems, ulong currentCount) { - _percent = Calculate(availableItems, currentCount); + CurrentPercent = Calculate(availableItems, currentCount); _index++; @@ -46,7 +45,7 @@ public override string ToString() if (DisplayPercent) { sBuilder.Append(' '); - sBuilder.Append(_percent.ToString()); + sBuilder.Append(CurrentPercent.ToString()); } return sBuilder.ToString(); @@ -56,7 +55,7 @@ private void Fill() { Array.Fill(_bar, ' '); - if (_percent.Value < 100) + if (CurrentPercent.Value < 100) _bar[_index] = _progressSymbol; } } diff --git a/src/Progress/Components/Spinner.cs b/src/Progress/Components/Spinner.cs index a70972a..46e5e06 100644 --- a/src/Progress/Components/Spinner.cs +++ b/src/Progress/Components/Spinner.cs @@ -7,13 +7,11 @@ internal class Spinner : Component private int _counter; private char _current; - private Percent _percent = default!; - public bool DisplayPercent { get; init; } = true; public override Component Next(ulong availableItems, ulong currentCount) { - _percent = Calculate(availableItems, currentCount); + CurrentPercent = Calculate(availableItems, currentCount); return this; } @@ -27,7 +25,7 @@ public override string ToString() if (DisplayPercent) { sBuilder.Append(' '); - sBuilder.Append(_percent.ToString()); + sBuilder.Append(CurrentPercent.ToString()); } return sBuilder.ToString(); diff --git a/src/Progress/Progress.csproj b/src/Progress/Progress.csproj index abbad70..7a4bfbc 100644 --- a/src/Progress/Progress.csproj +++ b/src/Progress/Progress.csproj @@ -9,7 +9,7 @@ Progress - 1.1.0 + 1.2.0 MIT Gerard Castello Report progress with ease diff --git a/src/Progress/Reporter.cs b/src/Progress/Reporter.cs index 1bfcf9e..cb629a9 100644 --- a/src/Progress/Reporter.cs +++ b/src/Progress/Reporter.cs @@ -17,7 +17,7 @@ public class Reporter : IDisposable private ulong _successCount; private ulong _failureCount; private Component _component; - private DateTimeOffset _startedOn = default!; + private Timer _timer; private Thread _reportingThread = default!; private CancellationTokenSource _cancellationTokenSource = default!; private CancellationToken _cancellationToken; @@ -26,7 +26,9 @@ public class Reporter : IDisposable /// Inidicates whehter the task is completed or not. /// public bool IsFinished => CurrentCount == _itemsCount; - + + internal bool DisplayEstimatedTimeOfArrival { get; set; } = true; + internal bool DisplayRemainingTime { get; set; } = true; internal bool DisplayElapsedTime { get; set; } = true; internal bool DisplayStartingTime { get; set; } = true; internal bool DisplayItemsOverview { get; set; } = true; @@ -48,6 +50,7 @@ internal Reporter(ulong itemsCount, Component component) _itemsCount = itemsCount; _component = component; + _timer = Timer.Start(); } /// @@ -70,7 +73,7 @@ public void Start() _successCount = 0; _failureCount = 0; - _startedOn = DateTimeOffset.UtcNow; + _timer = Timer.Start(); _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token; _reportingThread = DoWork(); @@ -115,21 +118,38 @@ public void Resume() /// public override string ToString() { - var elapsedTime = DateTimeOffset.UtcNow - _startedOn; - StringBuilder sBuilder = new(); if (DisplayStartingTime) { string label = "Process started at:".PadRight(RIGHT_PADDING); - sBuilder.AppendLine($"{label} {_startedOn.ToString().PadLeft(LEFT_PADDING)}"); + sBuilder.AppendLine($"{label} {_timer.StartedOn.ToString().PadLeft(LEFT_PADDING)}"); + } + if (DisplayEstimatedTimeOfArrival) + { + string label = "ETA:".PadRight(RIGHT_PADDING); + string value = _component.CurrentPercent.Value == 0 + ? Timer.Unknowm + : _timer.GetEstimatedTimeOfArrival(_component.CurrentPercent.Value).ToString(); + + sBuilder.AppendLine($"{label} {value.PadLeft(LEFT_PADDING)}"); } if (DisplayElapsedTime) { string label = "Elapsed time:".PadRight(RIGHT_PADDING); - sBuilder.AppendLine($"{label} {elapsedTime.ToString().PadLeft(LEFT_PADDING)}"); + sBuilder.AppendLine($"{label} {_timer.ElapsedTime.ToString().PadLeft(LEFT_PADDING)}"); + } + + if (DisplayRemainingTime) + { + string label = "Remaining time:".PadRight(RIGHT_PADDING); + string value = _component.CurrentPercent.Value == 0 + ? Timer.Unknowm + : _timer.GetRemainingTime(_component.CurrentPercent.Value).ToString(); + + sBuilder.AppendLine($"{label} {value.PadLeft(LEFT_PADDING)}"); } if (DisplayItemsSummary) diff --git a/src/Progress/ReporterBuilder.cs b/src/Progress/ReporterBuilder.cs index 7a05819..376cc45 100644 --- a/src/Progress/ReporterBuilder.cs +++ b/src/Progress/ReporterBuilder.cs @@ -8,6 +8,8 @@ namespace Progress; /// public class ReporterBuilder { + private bool _displayRemainingTime; + private bool _displayEstTimeOfArrival; private bool _displayElapsedTime; private bool _displayStartingTime; private bool _displayItemsOverview; @@ -25,6 +27,26 @@ public ReporterBuilder DisplayingElapsedTime() return this; } + /// + /// The reporter will display the remaining time to finish. + /// + /// + public ReporterBuilder DisplayingRemainingTime() + { + _displayRemainingTime = true; + return this; + } + + /// + /// The reporter will display when the operation is expected to finish. + /// + /// + public ReporterBuilder DisplayingTimeOfArrival() + { + _displayEstTimeOfArrival = true; + return this; + } + /// /// The reporter will display when the operation started. /// @@ -93,6 +115,8 @@ public Reporter Build(ulong itemsCount) return new Reporter(itemsCount, component) { + DisplayRemainingTime = _displayRemainingTime, + DisplayEstimatedTimeOfArrival = _displayEstTimeOfArrival, DisplayElapsedTime = _displayElapsedTime, DisplayStartingTime = _displayStartingTime, DisplayItemsOverview = _displayItemsOverview, diff --git a/src/Progress/Timer.cs b/src/Progress/Timer.cs new file mode 100644 index 0000000..cc46668 --- /dev/null +++ b/src/Progress/Timer.cs @@ -0,0 +1,30 @@ +namespace Progress; + +internal class Timer(DateTimeOffset startedOn) +{ + public const string Unknowm = "Unknowm"; + + private readonly DateTimeOffset _startedOn = startedOn; + + public TimeSpan ElapsedTime => DateTimeOffset.UtcNow - _startedOn; + public DateTimeOffset StartedOn => _startedOn; + + public static Timer Start() => new(DateTimeOffset.UtcNow); + + public TimeSpan GetRemainingTime(double currentPercent) + { + var eta = GetEstimatedTimeOfArrival(currentPercent); + var remaining = eta - DateTimeOffset.Now; + + if (remaining < TimeSpan.Zero) + return TimeSpan.Zero; + + return remaining; + } + + public DateTimeOffset GetEstimatedTimeOfArrival(double currentPercent) + { + double ms = ElapsedTime.TotalMilliseconds / currentPercent * 100; + return _startedOn.AddMilliseconds(ms); + } +} From 3b0bf25d9945b1c6332cfcfbd436a0e33c05b51f Mon Sep 17 00:00:00 2001 From: gcastellov Date: Thu, 28 Aug 2025 12:04:06 +0200 Subject: [PATCH 2/3] unit tests and fixes on Timer --- src/Progress.UnitTest/TimerTests.cs | 73 +++++++++++++++++++++++++++++ src/Progress/Timer.cs | 6 +++ 2 files changed, 79 insertions(+) create mode 100644 src/Progress.UnitTest/TimerTests.cs diff --git a/src/Progress.UnitTest/TimerTests.cs b/src/Progress.UnitTest/TimerTests.cs new file mode 100644 index 0000000..b883f3e --- /dev/null +++ b/src/Progress.UnitTest/TimerTests.cs @@ -0,0 +1,73 @@ +namespace Progress.UnitTest; + +public class TimerTests +{ + [Fact] + public void GivenNothingDone_WhenGettingRemainingTime_ThenReturnsMaxValue() + { + // Arrange + var timer = Timer.Start(); + + // Act + var actual = timer.GetRemainingTime(0); + + // Assert + actual.Should().Be(TimeSpan.MaxValue); + } + + + [Fact] + public void GivenEverythingDone_WhenGettingRemainingTime_ThenReturnsZero() + { + // Arrange + var timer = Timer.Start(); + + // Act + var actual = timer.GetRemainingTime(100); + + // Assert + actual.Should().Be(TimeSpan.Zero); + } + + [Fact] + public void GivenNothingDone_WhenGettingEstimatedTimeOfArrival_ThenReturnsMaxValue() + { + // Arrange + var timer = Timer.Start(); + + // Act + var actual = timer.GetEstimatedTimeOfArrival(0); + + // Assert + actual.Should().Be(DateTimeOffset.MaxValue); + } + + [Fact] + public void GivenInstance_WhenGettingStartedOn_ThenReturnsMomentOfCreation() + { + // Arrange + var moment = DateTimeOffset.UtcNow; + var timer = new Timer(moment); + + // Act + var actual = timer.StartedOn; + + // Assert + actual.Should().Be(moment); + } + + [Fact] + public async Task GivenSomeWorkDone_WhenGettingElapsedTime_ThenReturnsTime() + { + // Arrange + var timer = Timer.Start(); + var delay = TimeSpan.FromSeconds(1); + await Task.Delay(delay); + + // Act + var actual = timer.ElapsedTime; + + // Assert + actual.Should().BeGreaterThanOrEqualTo(delay); + } +} diff --git a/src/Progress/Timer.cs b/src/Progress/Timer.cs index cc46668..4549064 100644 --- a/src/Progress/Timer.cs +++ b/src/Progress/Timer.cs @@ -13,6 +13,9 @@ internal class Timer(DateTimeOffset startedOn) public TimeSpan GetRemainingTime(double currentPercent) { + if (currentPercent == 0) + return TimeSpan.MaxValue; + var eta = GetEstimatedTimeOfArrival(currentPercent); var remaining = eta - DateTimeOffset.Now; @@ -24,6 +27,9 @@ public TimeSpan GetRemainingTime(double currentPercent) public DateTimeOffset GetEstimatedTimeOfArrival(double currentPercent) { + if (currentPercent == 0) + return DateTimeOffset.MaxValue; + double ms = ElapsedTime.TotalMilliseconds / currentPercent * 100; return _startedOn.AddMilliseconds(ms); } From 846c98bec3a68c8514cca77d625c364a290e1f08 Mon Sep 17 00:00:00 2001 From: gcastellov Date: Fri, 29 Aug 2025 12:33:52 +0200 Subject: [PATCH 3/3] update README --- README.md | 6 ++---- src/Progress/Content/README.md | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dd20e8f..a42e63c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Report progression with ease by using multilple components such as Bars, Spinner using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) @@ -42,10 +44,6 @@ var descriptor = BarDescriptor.Default; ### Define the reporter to use the component ```csharp var reporter = new ReporterBuilder() - .DisplayingStartingTime() - .DisplayingElapsedTime() - .DisplayingItemsSummary() - .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(descriptor) .Build(Worker.AllItems); diff --git a/src/Progress/Content/README.md b/src/Progress/Content/README.md index 637f1ef..b0e74ad 100644 --- a/src/Progress/Content/README.md +++ b/src/Progress/Content/README.md @@ -9,6 +9,8 @@ Report progression with ease by using multilple components such as Bars, Spinner using var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) @@ -43,10 +45,12 @@ var descriptor = BarDescriptor.Default; var reporter = new ReporterBuilder() .DisplayingStartingTime() .DisplayingElapsedTime() + .DisplayingTimeOfArrival() + .DisplayingRemainingTime() .DisplayingItemsSummary() .DisplayingItemsOverview() .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) - .UsingComponentDescriptor(descriptor) + .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); ``` @@ -55,6 +59,16 @@ var reporter = new ReporterBuilder() reporter.Start(); ``` +### Stop the reporter +```csharp +reporter.Stop(); +``` + +### Resume the reporter +```csharp +reporter.Resume(); +``` + ### Report progress ```csharp reporter.ReportSuccess()