diff --git a/src/Progress.Samples.Bar.App/Program.cs b/src/Progress.Samples.Bar.App/Program.cs index e8bd44c..a86fe9b 100644 --- a/src/Progress.Samples.Bar.App/Program.cs +++ b/src/Progress.Samples.Bar.App/Program.cs @@ -1,6 +1,7 @@ using Progress; using Progress.Descriptors; using Progress.Samples; +using Progress.Settings; var onProgress = (Stats stats) => { @@ -21,6 +22,7 @@ .DisplayingItemsOverview() .NotifyingProgress(onProgress) .NotifyingCompletion(onCompletion) + .ExportingTo("output.json", FileType.Json) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(BarDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.HearthBeat.App/Program.cs b/src/Progress.Samples.HearthBeat.App/Program.cs index 5a0e56d..caff2cb 100644 --- a/src/Progress.Samples.HearthBeat.App/Program.cs +++ b/src/Progress.Samples.HearthBeat.App/Program.cs @@ -1,6 +1,7 @@ using Progress; using Progress.Descriptors; using Progress.Samples; +using Progress.Settings; var onProgress = (Stats stats) => { @@ -21,6 +22,7 @@ .DisplayingItemsOverview() .NotifyingProgress(onProgress) .NotifyingCompletion(onCompletion) + .ExportingTo("output.csv", FileType.Csv) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(HearthBeatDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Pulse.App/Program.cs b/src/Progress.Samples.Pulse.App/Program.cs index 3bb2dab..20b1d64 100644 --- a/src/Progress.Samples.Pulse.App/Program.cs +++ b/src/Progress.Samples.Pulse.App/Program.cs @@ -1,6 +1,7 @@ using Progress; using Progress.Descriptors; using Progress.Samples; +using Progress.Settings; var onProgress = (Stats stats) => { @@ -22,6 +23,7 @@ .DisplayingItemsOverview() .NotifyingProgress(onProgress) .NotifyingCompletion(onCompletion) + .ExportingTo("output.txt", FileType.Text) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(PulseDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.Samples.Spinner.App/Program.cs b/src/Progress.Samples.Spinner.App/Program.cs index d593589..6879109 100644 --- a/src/Progress.Samples.Spinner.App/Program.cs +++ b/src/Progress.Samples.Spinner.App/Program.cs @@ -22,6 +22,7 @@ .DisplayingItemsOverview() .NotifyingProgress(onProgress) .NotifyingCompletion(onCompletion) + .ExportingTo("output.xml", Progress.Settings.FileType.Xml) .UsingReportingFrequency(TimeSpan.FromMilliseconds(50)) .UsingComponentDescriptor(SpinnerDescriptor.Default) .Build(Worker.AllItems); diff --git a/src/Progress.UnitTest/ExporterTests.cs b/src/Progress.UnitTest/ExporterTests.cs new file mode 100644 index 0000000..ae72d62 --- /dev/null +++ b/src/Progress.UnitTest/ExporterTests.cs @@ -0,0 +1,38 @@ +using Progress.Settings; + +namespace Progress.UnitTest; + +public class ExporterTests +{ + private readonly Stats _stats = new() + { + StartedOn = DateTimeOffset.Now.AddMinutes(-1), + CurrentCount = 1000, + CurrentPercent = 100, + ElapsedTime = TimeSpan.FromMinutes(1), + EstTimeOfArrival = DateTimeOffset.UtcNow, + ExpectedItems = 1000, + FailureCount = 30, + SuccessCount = 970, + RemainingTime = TimeSpan.Zero + }; + + + [Theory] + [InlineData("output.csv", FileType.Csv)] + [InlineData("output.json", FileType.Json)] + [InlineData("output.txt", FileType.Text)] + [InlineData("output.xml", FileType.Xml)] + public void GivenStats_WhenExporting_ThenExportsSuccessfully(string fileName, FileType fileType) + { + // Arrange + ExportSettings settings = new(fileName, fileType); + Exporter exporter = new(settings); + + // Act + exporter.Export(_stats); + + // Assert + File.Exists(fileName).Should().BeTrue(); + } +} diff --git a/src/Progress.UnitTest/PrinterTests.cs b/src/Progress.UnitTest/PrinterTests.cs new file mode 100644 index 0000000..c1e5582 --- /dev/null +++ b/src/Progress.UnitTest/PrinterTests.cs @@ -0,0 +1,20 @@ +using Progress.Descriptors; + +namespace Progress.UnitTest; + +public class PrinterTests +{ + [Fact] + public void GivenDefaults_WhenPrinting_ThenGetsOutput() + { + // Arrange + Printer printer = new(new Settings.ReportingOptions(), BarDescriptor.Default.Build()); + Stats stats = new(); + + // Act + var actual = printer.Print(stats); + + // Assert + actual.Should().NotBeNullOrEmpty(); + } +} diff --git a/src/Progress.UnitTest/ReporterBuilderTests.cs b/src/Progress.UnitTest/ReporterBuilderTests.cs index 8c7340c..fc39528 100644 --- a/src/Progress.UnitTest/ReporterBuilderTests.cs +++ b/src/Progress.UnitTest/ReporterBuilderTests.cs @@ -1,4 +1,6 @@ -namespace Progress.UnitTest; +using Progress.Settings; + +namespace Progress.UnitTest; public class ReporterBuilderTests { @@ -37,7 +39,7 @@ public void GivenReportingFrequency_WhenBuilding_ThenReporterIsSetUp() .UsingReportingFrequency(expectedFrequency); // Act - var actual = builder.Build(100); + var actual = builder.Build(100).Configuration; // Assert actual.ReportFrequency.Should().Be(expectedFrequency); @@ -51,7 +53,7 @@ public void GivenElapsedTime_WhenBuilding_ThenReporterIsSetUp() .DisplayingElapsedTime(); // Act - var actual = builder.Build(100); + var actual = builder.Build(100).Configuration.Options; // Assert actual.DisplayElapsedTime.Should().BeTrue(); @@ -65,7 +67,7 @@ public void GivenStartingTime_WhenBuilding_ThenReporterIsSetUp() .DisplayingStartingTime(); // Act - var actual = builder.Build(100); + var actual = builder.Build(100).Configuration.Options; // Assert actual.DisplayStartingTime.Should().BeTrue(); @@ -79,7 +81,7 @@ public void GivenItemsOverview_WhenBuilding_ThenReporterIsSetUp() .DisplayingItemsOverview(); // Act - var actual = builder.Build(100); + var actual = builder.Build(100).Configuration.Options; // Assert actual.DisplayItemsOverview.Should().BeTrue(); @@ -93,7 +95,7 @@ public void GivenSummary_WhenBuilding_ThenReporterIsSetUp() .DisplayingItemsSummary(); // Act - var actual = builder.Build(100); + var actual = builder.Build(100).Configuration.Options; // Assert actual.DisplayItemsSummary.Should().BeTrue(); @@ -113,7 +115,7 @@ public void GivenProgressNotifications_WhenBuilding_ThenReporterIsSetUp() // Arrange actual.OnProgress.Should().NotBeNull(); actual.OnProgress.Should().Be(callback); - actual.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + actual.Configuration.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); } [Fact] @@ -131,7 +133,8 @@ public void GivenProgressNotificationsWithFrequency_WhenBuilding_ThenReporterIsS // Arrange actual.OnProgress.Should().NotBeNull(); actual.OnProgress.Should().Be(callback); - actual.StatsFrequency.Should().Be(frequency); + actual.Configuration.StatsFrequency.Should().Be(frequency); + actual.Configuration.Options.NotifyProgressStats.Should().BeTrue(); } [Fact] @@ -148,6 +151,25 @@ public void GivenCompletionNotifications_WhenBuilding_ThenReporterIsSetUp() // Arrange actual.OnCompletion.Should().NotBeNull(); actual.OnCompletion.Should().Be(callback); + actual.Configuration.Options.NotifyCompletionStats.Should().BeTrue(); } + [Fact] + public void GivenExporting_WhenBuilding_ThenReporterIsSetUp() + { + // Arrange + const string fileName = "output.txt"; + const FileType type = FileType.Text; + var builder = new ReporterBuilder() + .ExportingTo(fileName, type); + + // Act + var actual = builder.Build(100); + + // Assert + actual.Configuration.ExportSettings.Should().NotBeNull(); + actual.Configuration.ExportSettings.FileName.Should().Be(fileName); + actual.Configuration.ExportSettings.FileType.Should().Be(FileType.Text); + actual.Configuration.Options.ExportCompletionStats.Should().BeTrue(); + } } diff --git a/src/Progress.UnitTest/ReporterTests.cs b/src/Progress.UnitTest/ReporterTests.cs index 6b0ddd1..8d760af 100644 --- a/src/Progress.UnitTest/ReporterTests.cs +++ b/src/Progress.UnitTest/ReporterTests.cs @@ -19,16 +19,18 @@ public void GivenDefaults_WhenInitializing_ThenExpectedSettings() var reporter = new Reporter(100, BarDescriptor.Default.Build()); // Assert - reporter.DisplayStartingTime.Should().BeTrue(); - reporter.DisplayRemainingTime.Should().BeTrue(); - reporter.DisplayElapsedTime.Should().BeTrue(); - reporter.DisplayEstimatedTimeOfArrival.Should().BeTrue(); - reporter.DisplayItemsOverview.Should().BeTrue(); - reporter.DisplayItemsSummary.Should().BeTrue(); - reporter.NotifyProgressStats.Should().BeTrue(); - reporter.NotifyCompletionStats.Should().BeTrue(); - reporter.ReportFrequency.Should().Be(TimeSpan.FromSeconds(1)); - reporter.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + reporter.Configuration.Options.DisplayStartingTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayRemainingTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayElapsedTime.Should().BeTrue(); + reporter.Configuration.Options.DisplayEstimatedTimeOfArrival.Should().BeTrue(); + reporter.Configuration.Options.DisplayItemsOverview.Should().BeTrue(); + reporter.Configuration.Options.DisplayItemsSummary.Should().BeTrue(); + reporter.Configuration.Options.NotifyProgressStats.Should().BeFalse(); + reporter.Configuration.Options.NotifyCompletionStats.Should().BeFalse(); + reporter.Configuration.Options.ExportCompletionStats.Should().BeFalse(); + reporter.Configuration.ReportFrequency.Should().Be(TimeSpan.FromSeconds(1)); + reporter.Configuration.StatsFrequency.Should().Be(TimeSpan.FromSeconds(5)); + reporter.Configuration.ExportSettings.Should().BeNull(); reporter.OnProgress.Should().BeNull(); reporter.OnCompletion.Should().BeNull(); } @@ -123,10 +125,11 @@ public async Task GivenProgressNotifications_WhenRunning_ThenCallbackIsCalled() bool isCalled = false; var reporter = new Reporter(100, BarDescriptor.Default.Build()) { - NotifyProgressStats = true, - StatsFrequency = TimeSpan.FromSeconds(1), OnProgress = (stats) => isCalled = true }; + + reporter.Configuration.Options.NotifyProgressStats = true; + reporter.Configuration.StatsFrequency = TimeSpan.FromSeconds(1); // Act @@ -144,10 +147,11 @@ public async Task GivenCompletionNotifications_WhenFinished_ThenCallbackIsCalled bool isCalled = false; var reporter = new Reporter(1, BarDescriptor.Default.Build()) { - NotifyCompletionStats = true, - ReportFrequency = TimeSpan.FromMicroseconds(500), OnCompletion = (stats) => isCalled = true }; + + reporter.Configuration.Options.NotifyCompletionStats = true; + reporter.Configuration.ReportFrequency = TimeSpan.FromMicroseconds(500); reporter.Start(); diff --git a/src/Progress/Components/Component.cs b/src/Progress/Components/Component.cs index 0182df2..5d53f5d 100644 --- a/src/Progress/Components/Component.cs +++ b/src/Progress/Components/Component.cs @@ -6,7 +6,7 @@ internal abstract class Component { private static readonly CultureInfo Culture = new("en-US"); - public Percent CurrentPercent { get; protected set; } = default!; + public Percent CurrentPercent { get; protected set; } = Percent.Zero; public abstract Component Next(ulong availableItems, ulong currentCount); @@ -14,6 +14,8 @@ internal abstract class Component internal class Percent { + public static Percent Zero => new(0, 0); + private readonly double _percent; public Percent(ulong items, ulong count) diff --git a/src/Progress/Exporter.cs b/src/Progress/Exporter.cs new file mode 100644 index 0000000..7bd9b5b --- /dev/null +++ b/src/Progress/Exporter.cs @@ -0,0 +1,26 @@ +using Progress.Exporters; +using Progress.Settings; +using System.Text; + +namespace Progress; + +/// +/// Exports the operation result in different formats +/// +internal class Exporter(ExportSettings settings) +{ + private readonly ExportSettings _settings = settings; + private readonly IEnumerable _exporters = [ new CsvExporter(), new TextExporter(), new JsonExporter(), new XmlExporter() ]; + + public void Export(Stats stats) + { + using FileStream stream = File.Exists(_settings.FileName) + ? File.OpenWrite(_settings.FileName) + : File.Create(_settings.FileName); + + IContentExporter contentExporter = _exporters.FirstOrDefault(e => e.FileType == _settings.FileType) ?? throw new NotSupportedException("Not supported export type"); + string content = contentExporter.Export(stats); + byte[] result = Encoding.Default.GetBytes(content); + stream.Write(result, 0, result.Length); + } +} diff --git a/src/Progress/Exporters/CsvExporter.cs b/src/Progress/Exporters/CsvExporter.cs new file mode 100644 index 0000000..44d3b61 --- /dev/null +++ b/src/Progress/Exporters/CsvExporter.cs @@ -0,0 +1,30 @@ +using Progress.Settings; +using System.Text; + +namespace Progress.Exporters +{ + internal class CsvExporter : IContentExporter + { + private const char Separator = ';'; + + public FileType FileType => FileType.Csv; + + public string Export(Stats stats) + { + var type = stats.GetType(); + string[] properyNames = type.GetProperties().Select(p => p.Name).Order().ToArray(); + + StringBuilder sBuilder = new(); + sBuilder.AppendLine(string.Join(Separator, properyNames)); + + foreach(string properyName in properyNames) + { + object value = type.GetProperty(properyName)!.GetValue(stats)!; + sBuilder.Append(value.ToString()); + sBuilder.Append(Separator); + } + + return sBuilder.ToString(); + } + } +} diff --git a/src/Progress/Exporters/IContentExporter.cs b/src/Progress/Exporters/IContentExporter.cs new file mode 100644 index 0000000..4e9d433 --- /dev/null +++ b/src/Progress/Exporters/IContentExporter.cs @@ -0,0 +1,10 @@ +using Progress.Settings; + +namespace Progress.Exporters +{ + internal interface IContentExporter + { + FileType FileType { get; } + string Export(Stats stats); + } +} diff --git a/src/Progress/Exporters/JsonExporter.cs b/src/Progress/Exporters/JsonExporter.cs new file mode 100644 index 0000000..ab256d8 --- /dev/null +++ b/src/Progress/Exporters/JsonExporter.cs @@ -0,0 +1,12 @@ +using Progress.Settings; +using System.Text.Json; + +namespace Progress.Exporters +{ + internal class JsonExporter : IContentExporter + { + public FileType FileType => FileType.Json; + + public string Export(Stats stats) => JsonSerializer.Serialize(stats); + } +} diff --git a/src/Progress/Exporters/TextExporter.cs b/src/Progress/Exporters/TextExporter.cs new file mode 100644 index 0000000..32db359 --- /dev/null +++ b/src/Progress/Exporters/TextExporter.cs @@ -0,0 +1,31 @@ +using Progress.Settings; +using System.Text; + +namespace Progress.Exporters +{ + internal class TextExporter : IContentExporter + { + private const int LeftPadding = 30; + private const int RightPadding = 20; + + public FileType FileType => FileType.Text; + + public string Export(Stats stats) + { + var type = stats.GetType(); + string[] properyNames = type.GetProperties().Select(p => p.Name).Order().ToArray(); + + StringBuilder sBuilder = new(); + + foreach (string properyName in properyNames) + { + object value = type.GetProperty(properyName)!.GetValue(stats)!; + sBuilder.Append($"{properyName}:".PadRight(RightPadding)); + sBuilder.Append(value.ToString()!.PadLeft(LeftPadding)); + sBuilder.AppendLine(); + } + + return sBuilder.ToString(); + } + } +} diff --git a/src/Progress/Exporters/XmlExporter.cs b/src/Progress/Exporters/XmlExporter.cs new file mode 100644 index 0000000..a413f5c --- /dev/null +++ b/src/Progress/Exporters/XmlExporter.cs @@ -0,0 +1,29 @@ +using Progress.Settings; +using System.Xml; + +namespace Progress.Exporters; + +internal class XmlExporter : IContentExporter +{ + public FileType FileType => FileType.Xml; + + public string Export(Stats stats) + { + var type = stats.GetType(); + string[] properyNames = type.GetProperties().Select(p => p.Name).Order().ToArray(); + + XmlDocument xmlDoc = new(); + var root = xmlDoc.CreateElement("Stats"); + + foreach (string properyName in properyNames) + { + object value = type.GetProperty(properyName)!.GetValue(stats)!; + var xmlEntry = xmlDoc.CreateElement(properyName); + xmlEntry.InnerText = value.ToString()!; + root.AppendChild(xmlEntry); + } + + xmlDoc.AppendChild(root); + return xmlDoc.OuterXml; + } +} diff --git a/src/Progress/Printer.cs b/src/Progress/Printer.cs new file mode 100644 index 0000000..d859abb --- /dev/null +++ b/src/Progress/Printer.cs @@ -0,0 +1,69 @@ +using Progress.Components; +using Progress.Settings; +using System.Text; + +namespace Progress; + +internal class Printer(ReportingOptions options, Component component) +{ + private const int Left_Padding = 30; + private const int Right_Padding = 20; + + private readonly ReportingOptions _options = options; + private readonly Component _component = component; + + public string Print(Stats stats) + { + StringBuilder sBuilder = new(); + + if (_options.DisplayStartingTime) + { + string label = "Process started at:".PadRight(Right_Padding); + sBuilder.AppendLine($"{label} {stats.StartedOn.ToString().PadLeft(Left_Padding)}"); + } + + if (_options.DisplayEstimatedTimeOfArrival) + { + string label = "ETA:".PadRight(Right_Padding); + string value = stats.CurrentPercent == 0 + ? Timer.Unknowm + : stats.EstTimeOfArrival.ToString(); + + sBuilder.AppendLine($"{label} {value.PadLeft(Left_Padding)}"); + } + + if (_options.DisplayElapsedTime) + { + string label = "Elapsed time:".PadRight(Right_Padding); + sBuilder.AppendLine($"{label} {stats.ElapsedTime.ToString().PadLeft(Left_Padding)}"); + } + + if (_options.DisplayRemainingTime) + { + string label = "Remaining time:".PadRight(Right_Padding); + string value = stats.CurrentPercent == 0 + ? Timer.Unknowm + : stats.RemainingTime.ToString(); + + sBuilder.AppendLine($"{label} {value.PadLeft(Left_Padding)}"); + } + + if (_options.DisplayItemsSummary) + { + string successLabel = "Successful items:".PadRight(Right_Padding); + string unsuccessLabel = "Unsuccessful items:".PadRight(Right_Padding); + sBuilder.AppendLine($"{successLabel} {stats.SuccessCount.ToString().PadLeft(Left_Padding)}"); + sBuilder.AppendLine($"{unsuccessLabel} {stats.FailureCount.ToString().PadLeft(Left_Padding)}"); + } + + if (_options.DisplayItemsOverview) + { + string label = "Total items:".PadRight(Right_Padding); + sBuilder.AppendLine($"{label} {stats.CurrentCount.ToString().PadLeft(Left_Padding)}"); + } + + sBuilder.AppendLine(); + sBuilder.AppendLine(_component.ToString()); + return sBuilder.ToString(); + } +} diff --git a/src/Progress/Progress.csproj b/src/Progress/Progress.csproj index 4827469..78a594e 100644 --- a/src/Progress/Progress.csproj +++ b/src/Progress/Progress.csproj @@ -9,7 +9,7 @@ Progress - 1.3.0 + 1.4.0 MIT Gerard Castello Report progress with ease diff --git a/src/Progress/Reporter.cs b/src/Progress/Reporter.cs index 5b446f7..9906f3a 100644 --- a/src/Progress/Reporter.cs +++ b/src/Progress/Reporter.cs @@ -1,4 +1,5 @@ using Progress.Components; +using Progress.Settings; using System.Text; namespace Progress; @@ -8,16 +9,15 @@ namespace Progress; /// public class Reporter : IDisposable { - private const int LEFT_PADDING = 30; - private const int RIGHT_PADDING = 20; - private readonly ulong _itemsCount; + private readonly Configuration _configuration; private bool _isDisposed; private ulong _successCount; private ulong _failureCount; private Component _component; private Timer _timer; + private Printer _printer; private Thread _reportingThread = default!; private Thread _statsThread = default!; private CancellationTokenSource _cancellationTokenSource = default!; @@ -29,31 +29,10 @@ public class Reporter : IDisposable /// public bool IsFinished => CurrentCount == _itemsCount; - /// - /// Gets or sets the progress notification hook which invocation happens during the operation. - /// - public Action? OnProgress { get; set; } = null!; + internal Configuration Configuration => _configuration; + internal Action? OnProgress { get; set; } = null!; + internal Action? OnCompletion { get; set; } = null!; - /// - /// Gets or sets the completion hook which invocation happens at the end of the operation. - /// - public Action? OnCompletion { get; set; } = null!; - - 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; - internal bool DisplayItemsSummary { get; set; } = true; - internal bool NotifyProgressStats { get; set; } = true; - internal bool NotifyCompletionStats { get; set; } = true; - - internal TimeSpan ReportFrequency { get; set; } = TimeSpan.FromSeconds(1); - internal TimeSpan StatsFrequency { get; set; } = TimeSpan.FromSeconds(5); - - private string AllItems => (_successCount + _failureCount).ToString().PadLeft(10); - private string SuccessfulItems => _successCount.ToString().PadLeft(10); - private string UnsuccessfulItems => _failureCount.ToString().PadLeft(10); private ulong CurrentCount => _successCount + _failureCount; @@ -68,6 +47,8 @@ internal Reporter(ulong itemsCount, Component component) _itemsCount = itemsCount; _component = component; _timer = Timer.Start(); + _configuration = new Configuration(); + _printer = new Printer(Configuration.Options, component); } /// @@ -97,7 +78,7 @@ public void Start() _reportingThread = DoWork(); _reportingThread.Start(); - if (NotifyProgressStats) + if (Configuration.Options.NotifyProgressStats) { _statsThread = DoStats(); _statsThread.Start(); @@ -138,7 +119,7 @@ public void Resume() _reportingThread = DoWork(); _reportingThread.Start(); - if (NotifyProgressStats) + if (Configuration.Options.NotifyProgressStats) { _statsThread = DoStats(); _statsThread.Start(); @@ -151,57 +132,8 @@ public void Resume() /// public override string ToString() { - StringBuilder sBuilder = new(); - - if (DisplayStartingTime) - { - string label = "Process started at:".PadRight(RIGHT_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} {_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) - { - string successLabel = "Successful items:".PadRight(RIGHT_PADDING); - string unsuccessLabel = "Unsuccessful items:".PadRight(RIGHT_PADDING); - sBuilder.AppendLine($"{successLabel} {SuccessfulItems.ToString().PadLeft(LEFT_PADDING)}"); - sBuilder.AppendLine($"{unsuccessLabel} {UnsuccessfulItems.ToString().PadLeft(LEFT_PADDING)}"); - } - - if (DisplayItemsOverview) - { - string label = "Total items:".PadRight(RIGHT_PADDING); - sBuilder.AppendLine($"{label} {AllItems.ToString().PadLeft(LEFT_PADDING)}"); - } - - sBuilder.AppendLine(); - sBuilder.AppendLine(_component.ToString()); - return sBuilder.ToString(); + var stats = CollectStats(); + return _printer.Print(stats); } /// @@ -248,7 +180,7 @@ private void Display() private void ReportStats() { - if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < StatsFrequency) + if (OnProgress == null || DateTimeOffset.UtcNow - _lastStatsNotification < Configuration.StatsFrequency) return; var stats = CollectStats(); @@ -263,16 +195,19 @@ private Thread DoWork() do { Display(); - Thread.Sleep(ReportFrequency); + Thread.Sleep(Configuration.ReportFrequency); } while (!IsFinished && !_cancellationToken.IsCancellationRequested); Display(); - if (IsFinished && NotifyCompletionStats) + if (IsFinished && (Configuration.Options.NotifyCompletionStats || Configuration.Options.ExportCompletionStats)) { var stats = CollectStats(); OnCompletion?.Invoke(stats); + + if (Configuration.ExportSettings != null) + new Exporter(Configuration.ExportSettings).Export(stats); } }); @@ -286,7 +221,7 @@ private Thread DoStats() do { ReportStats(); - Thread.Sleep(StatsFrequency); + Thread.Sleep(Configuration.StatsFrequency); } while (!IsFinished && !_cancellationToken.IsCancellationRequested); }); diff --git a/src/Progress/ReporterBuilder.cs b/src/Progress/ReporterBuilder.cs index 83547bb..392dbd4 100644 --- a/src/Progress/ReporterBuilder.cs +++ b/src/Progress/ReporterBuilder.cs @@ -1,4 +1,5 @@ using Progress.Descriptors; +using Progress.Settings; namespace Progress; @@ -17,6 +18,7 @@ public class ReporterBuilder private TimeSpan _reportFrequency = TimeSpan.FromSeconds(1); private TimeSpan _statsFrequency = TimeSpan.FromSeconds(5); private ComponentDescriptor _componentDescriptor = BarDescriptor.Default; + private ExportSettings _exportSettings = default!; private Action _onProgressNotified = default!; private Action _onCompletionNotified = default!; @@ -138,6 +140,18 @@ public ReporterBuilder NotifyingCompletion(Action callback) return this; } + /// + /// Sets and tells the reporter how and where to export the final stats. + /// + /// + /// + /// + public ReporterBuilder ExportingTo(string fileName, FileType fileType) + { + _exportSettings = new ExportSettings(fileName, fileType); + return this; + } + /// /// Builds the reporte getting an instance of . /// @@ -150,21 +164,26 @@ public Reporter Build(ulong itemsCount) throw new ArgumentException("Nothing to do! Set the initial items count for completion."); var component = _componentDescriptor.Build(); - - return new Reporter(itemsCount, component) + + var reporter = new Reporter(itemsCount, component) { - DisplayRemainingTime = _displayRemainingTime, - DisplayEstimatedTimeOfArrival = _displayEstTimeOfArrival, - DisplayElapsedTime = _displayElapsedTime, - DisplayStartingTime = _displayStartingTime, - DisplayItemsOverview = _displayItemsOverview, - DisplayItemsSummary = _displayItemsSummary, - NotifyProgressStats = _onProgressNotified != null, - NotifyCompletionStats = _onCompletionNotified != null, - ReportFrequency = _reportFrequency, - StatsFrequency = _statsFrequency, OnProgress = _onProgressNotified, OnCompletion = _onCompletionNotified }; + + reporter.Configuration.ReportFrequency = _reportFrequency; + reporter.Configuration.StatsFrequency = _statsFrequency; + reporter.Configuration.ExportSettings = _exportSettings; + reporter.Configuration.Options.DisplayRemainingTime = _displayRemainingTime; + reporter.Configuration.Options.DisplayEstimatedTimeOfArrival = _displayEstTimeOfArrival; + reporter.Configuration.Options.DisplayElapsedTime = _displayElapsedTime; + reporter.Configuration.Options.DisplayStartingTime = _displayStartingTime; + reporter.Configuration.Options.DisplayItemsOverview = _displayItemsOverview; + reporter.Configuration.Options.DisplayItemsSummary = _displayItemsSummary; + reporter.Configuration.Options.NotifyProgressStats = _onProgressNotified != null; + reporter.Configuration.Options.NotifyCompletionStats = _onCompletionNotified != null; + reporter.Configuration.Options.ExportCompletionStats = _exportSettings != null; + + return reporter; } } diff --git a/src/Progress/Settings/Configuration.cs b/src/Progress/Settings/Configuration.cs new file mode 100644 index 0000000..0f4825b --- /dev/null +++ b/src/Progress/Settings/Configuration.cs @@ -0,0 +1,9 @@ +namespace Progress.Settings; + +internal class Configuration +{ + public ReportingOptions Options { get; init; } = new(); + public ExportSettings ExportSettings { get; set; } = default!; + public TimeSpan ReportFrequency { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan StatsFrequency { get; set; } = TimeSpan.FromSeconds(5); +} diff --git a/src/Progress/Settings/ExportSettings.cs b/src/Progress/Settings/ExportSettings.cs new file mode 100644 index 0000000..c6778e9 --- /dev/null +++ b/src/Progress/Settings/ExportSettings.cs @@ -0,0 +1,26 @@ +namespace Progress.Settings; + +/// +/// Supported export file types +/// +public enum FileType +{ + /// + /// CSV + /// + Csv, + /// + /// TXT + /// + Text, + /// + /// JSON + /// + Json, + /// + /// XML + /// + Xml +} + +internal record ExportSettings(string FileName, FileType FileType); \ No newline at end of file diff --git a/src/Progress/Settings/ReportingOptions.cs b/src/Progress/Settings/ReportingOptions.cs new file mode 100644 index 0000000..91a5a5b --- /dev/null +++ b/src/Progress/Settings/ReportingOptions.cs @@ -0,0 +1,14 @@ +namespace Progress.Settings; + +internal class ReportingOptions +{ + 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; + internal bool DisplayItemsSummary { get; set; } = true; + internal bool NotifyProgressStats { get; set; } = false; + internal bool NotifyCompletionStats { get; set; } = false; + internal bool ExportCompletionStats { get; set; } = false; +}