diff --git a/RombaSharp/Features/DatStats.cs b/RombaSharp/Features/DatStats.cs index 2f936fc6..537ae920 100644 --- a/RombaSharp/Features/DatStats.cs +++ b/RombaSharp/Features/DatStats.cs @@ -33,7 +33,14 @@ namespace RombaSharp.Features Inputs = new List { Path.GetFullPath(_dats) }; // Now output the stats for all inputs - Statistics.OutputStats(Inputs, "rombasharp-datstats", null /* outDir */, true /* single */, true /* baddumpCol */, true /* nodumpCol */, StatReportFormat.Textfile); + var statistics = Statistics.CalculateStatistics(Inputs, single: true); + Statistics.Write( + statistics, + "rombasharp-datstats", + outDir: null, + baddumpCol: true, + nodumpCol: true, + StatReportFormat.Textfile); } } } diff --git a/SabreTools.DatTools/Statistics.cs b/SabreTools.DatTools/Statistics.cs index 37b8e1e4..3811a976 100644 --- a/SabreTools.DatTools/Statistics.cs +++ b/SabreTools.DatTools/Statistics.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Threading.Tasks; +using SabreTools.Core; using SabreTools.DatFiles; using SabreTools.DatItems; using SabreTools.IO; @@ -28,36 +30,15 @@ namespace SabreTools.DatTools #endregion /// - /// Output the stats for a list of input dats as files in a human-readable format + /// Calculate statistics from a list of inputs /// /// List of input files and folders - /// Name of the output file /// True if single DAT stats are output, false otherwise - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - /// Set the statistics output format to use - public static void OutputStats( - List inputs, - string reportName, - string outDir, - bool single, - bool baddumpCol, - bool nodumpCol, - StatReportFormat statDatFormat) + /// True if the error that is thrown should be thrown back to the caller, false otherwise + public static List CalculateStatistics(List inputs, bool single, bool throwOnError = false) { - // If there's no output format, set the default - if (statDatFormat == StatReportFormat.None) - statDatFormat = StatReportFormat.Textfile; - - // Get the proper output file name - if (string.IsNullOrWhiteSpace(reportName)) - reportName = "report"; - - // Get the proper output directory name - outDir = outDir.Ensure(); - - // Get the dictionary of desired output report names - Dictionary outputs = CreateOutStatsNames(outDir, statDatFormat, reportName); + // Create the output list + List stats = new List(); // Make sure we have all files and then order them List files = PathTool.GetFilesOnly(inputs); @@ -66,99 +47,158 @@ namespace SabreTools.DatTools .ThenBy(i => Path.GetFileName(i.CurrentPath)) .ToList(); - // Get all of the writers that we need - List reports = outputs.Select(kvp => BaseReport.Create(kvp.Key, kvp.Value, baddumpCol, nodumpCol)).ToList(); - - // Write the header, if any - reports.ForEach(report => report.WriteHeader()); - - // Init all total variables - ItemDictionary totalStats = new ItemDictionary(); + // Init total + DatStatistics totalStats = new DatStatistics + { + Statistics = new ItemDictionary(), + DisplayName = "DIR: All DATs", + MachineCount = 0, + IsDirectory = true, + }; // Init directory-level variables string lastdir = null; - string basepath = null; - ItemDictionary dirStats = new ItemDictionary(); + DatStatistics dirStats = new DatStatistics + { + Statistics = new ItemDictionary(), + MachineCount = 0, + IsDirectory = true, + }; // Now process each of the input files foreach (ParentablePath file in files) { // Get the directory for the current file string thisdir = Path.GetDirectoryName(file.CurrentPath); - basepath = Path.GetDirectoryName(Path.GetDirectoryName(file.CurrentPath)); // If we don't have the first file and the directory has changed, show the previous directory stats and reset - if (lastdir != null && thisdir != lastdir) + if (lastdir != null && thisdir != lastdir && single) { - // Output separator if needed - reports.ForEach(report => report.WriteMidSeparator()); - - DatFile lastdirdat = DatFile.Create(); - - reports.ForEach(report => report.ReplaceStatistics($"DIR: {WebUtility.HtmlEncode(lastdir)}", dirStats.GameCount, dirStats)); - reports.ForEach(report => report.Write()); - - // Write the mid-footer, if any - reports.ForEach(report => report.WriteFooterSeparator()); - - // Write the header, if any - reports.ForEach(report => report.WriteMidHeader()); - - // Reset the directory stats - dirStats.ResetStatistics(); + dirStats.DisplayName = $"DIR: {WebUtility.HtmlEncode(lastdir)}"; + dirStats.MachineCount = dirStats.Statistics.GameCount; + stats.Add(dirStats); + dirStats = new DatStatistics + { + Statistics = new ItemDictionary(), + MachineCount = 0, + IsDirectory = true, + }; } - logger.Verbose($"Beginning stat collection for '{file.CurrentPath}'"); - List games = new List(); - DatFile datdata = Parser.CreateAndParse(file.CurrentPath, statsOnly: true); + InternalStopwatch watch = new InternalStopwatch($"Collecting statistics for '{file.CurrentPath}'"); + + List machines = new List(); + DatFile datdata = Parser.CreateAndParse(file.CurrentPath, statsOnly: true, throwOnError: throwOnError); datdata.Items.BucketBy(ItemKey.Machine, DedupeType.None, norename: true); - // Output single DAT stats (if asked) - logger.User($"Adding stats for file '{file.CurrentPath}'\n"); + // Add single DAT stats (if asked) if (single) { - reports.ForEach(report => report.ReplaceStatistics(datdata.Header.FileName, datdata.Items.Keys.Count, datdata.Items)); - reports.ForEach(report => report.Write()); + DatStatistics individualStats = new DatStatistics + { + Statistics = datdata.Items, + DisplayName = datdata.Header.FileName, + MachineCount = datdata.Items.Keys.Count, + IsDirectory = false, + }; + stats.Add(individualStats); } // Add single DAT stats to dir - dirStats.AddStatistics(datdata.Items); - dirStats.GameCount += datdata.Items.Keys.Count(); + dirStats.Statistics.AddStatistics(datdata.Items); + dirStats.Statistics.GameCount += datdata.Items.Keys.Count(); // Add single DAT stats to totals - totalStats.AddStatistics(datdata.Items); - totalStats.GameCount += datdata.Items.Keys.Count(); + totalStats.Statistics.AddStatistics(datdata.Items); + totalStats.Statistics.GameCount += datdata.Items.Keys.Count(); // Make sure to assign the new directory lastdir = thisdir; + + watch.Stop(); } - // Output the directory stats one last time - reports.ForEach(report => report.WriteMidSeparator()); - + // Add last directory stats if (single) { - reports.ForEach(report => report.ReplaceStatistics($"DIR: {WebUtility.HtmlEncode(lastdir)}", dirStats.GameCount, dirStats)); - reports.ForEach(report => report.Write()); + dirStats.DisplayName = $"DIR: {WebUtility.HtmlEncode(lastdir)}"; + dirStats.MachineCount = dirStats.Statistics.GameCount; + stats.Add(dirStats); } - // Write the mid-footer, if any - reports.ForEach(report => report.WriteFooterSeparator()); + // Add total DAT stats + totalStats.MachineCount = totalStats.Statistics.GameCount; + stats.Add(totalStats); - // Write the header, if any - reports.ForEach(report => report.WriteMidHeader()); + return stats; + } - // Reset the directory stats - dirStats.ResetStatistics(); + /// + /// Output the stats for a list of input dats as files in a human-readable format + /// + /// List of pre-calculated statistics objects + /// Name of the output file + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + /// Set the statistics output format to use + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// True if the report was written correctly, false otherwise + public static bool Write( + List stats, + string reportName, + string outDir, + bool baddumpCol, + bool nodumpCol, + StatReportFormat statDatFormat, + bool throwOnError = false) + { + // If there's no output format, set the default + if (statDatFormat == StatReportFormat.None) + { + logger.Verbose("No report format defined, defaulting to textfile"); + statDatFormat = StatReportFormat.Textfile; + } - // Output total DAT stats - reports.ForEach(report => report.ReplaceStatistics("DIR: All DATs", totalStats.GameCount, totalStats)); - reports.ForEach(report => report.Write()); + // Get the proper output file name + if (string.IsNullOrWhiteSpace(reportName)) + reportName = "report"; - // Output footer if needed - reports.ForEach(report => report.WriteFooter()); + // Get the proper output directory name + outDir = outDir.Ensure(); - logger.User($"{Environment.NewLine}Please check the log folder if the stats scrolled offscreen"); + InternalStopwatch watch = new InternalStopwatch($"Writing out report data to '{outDir}'"); + + // Get the dictionary of desired output report names + Dictionary outfiles = CreateOutStatsNames(outDir, statDatFormat, reportName); + + try + { + // Write out all required formats + Parallel.ForEach(outfiles.Keys, Globals.ParallelOptions, reportFormat => + { + string outfile = outfiles[reportFormat]; + try + { + BaseReport.Create(reportFormat, stats)?.WriteToFile(outfile, baddumpCol, nodumpCol, throwOnError); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex, $"Report '{outfile}' could not be written out"); + } + + }); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + finally + { + watch.Stop(); + } + + return true; } /// diff --git a/SabreTools.DatTools/Writer.cs b/SabreTools.DatTools/Writer.cs index 044306d8..584a4698 100644 --- a/SabreTools.DatTools/Writer.cs +++ b/SabreTools.DatTools/Writer.cs @@ -116,8 +116,18 @@ namespace SabreTools.DatTools datFile.Items.BucketBy(ItemKey.Machine, DedupeType.None, norename: true); - var consoleOutput = BaseReport.Create(StatReportFormat.None, null, true, true); - consoleOutput.ReplaceStatistics(datFile.Header.FileName, datFile.Items.Keys.Count(), datFile.Items); + var statsList = new List + { + new DatStatistics + { + Statistics = datFile.Items, + DisplayName = datFile.Header.FileName, + MachineCount = datFile.Items.Keys.Count(), + IsDirectory = false, + }, + }; + var consoleOutput = BaseReport.Create(StatReportFormat.None, statsList); + consoleOutput.WriteToFile(null, true, true); } /// diff --git a/SabreTools.Reports/BaseReport.cs b/SabreTools.Reports/BaseReport.cs index 1ef898bb..31472760 100644 --- a/SabreTools.Reports/BaseReport.cs +++ b/SabreTools.Reports/BaseReport.cs @@ -1,7 +1,6 @@ -using System; -using System.IO; +using System.Collections.Generic; -using SabreTools.DatFiles; +using SabreTools.Logging; using SabreTools.Reports.Formats; namespace SabreTools.Reports @@ -9,110 +8,57 @@ namespace SabreTools.Reports /// /// Base class for a report output format /// - /// TODO: Can this be overhauled to have all types write like DatFiles? public abstract class BaseReport { - protected string _name; - protected long _machineCount; - protected ItemDictionary _stats; + #region Logging - protected StreamWriter _writer; - protected bool _baddumpCol; - protected bool _nodumpCol; + /// + /// Logging object + /// + protected readonly Logger logger = new Logger(); + + #endregion + + public List Statistics { get; set; } /// /// Create a new report from the filename /// - /// Name of the file to write out to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public BaseReport(string filename, bool baddumpCol = false, bool nodumpCol = false) + /// List of statistics objects to set + public BaseReport(List statsList) { - var fs = File.Create(filename); - if (fs != null) - _writer = new StreamWriter(fs) { AutoFlush = true }; - - _baddumpCol = baddumpCol; - _nodumpCol = nodumpCol; - } - - /// - /// Create a new report from the stream - /// - /// Output stream to write to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public BaseReport(Stream stream, bool baddumpCol = false, bool nodumpCol = false) - { - if (!stream.CanWrite) - throw new ArgumentException(nameof(stream)); - - _writer = new StreamWriter(stream) { AutoFlush = true }; - _baddumpCol = baddumpCol; - _nodumpCol = nodumpCol; + Statistics = statsList; } /// /// Create a specific type of BaseReport to be used based on a format and user inputs /// /// Format of the Statistics Report to be created - /// Name of the file to write out to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise + /// List of statistics objects to set /// BaseReport of the specific internal type that corresponds to the inputs - public static BaseReport Create(StatReportFormat statReportFormat, string filename, bool baddumpCol, bool nodumpCol) + public static BaseReport Create(StatReportFormat statReportFormat, List statsList) { return statReportFormat switch { - StatReportFormat.None => new Textfile(Console.OpenStandardOutput(), baddumpCol, nodumpCol), - StatReportFormat.Textfile => new Textfile(filename, baddumpCol, nodumpCol), - StatReportFormat.CSV => new SeparatedValue(filename, ',', baddumpCol, nodumpCol), - StatReportFormat.HTML => new Html(filename, baddumpCol, nodumpCol), - StatReportFormat.SSV => new SeparatedValue(filename, ';', baddumpCol, nodumpCol), - StatReportFormat.TSV => new SeparatedValue(filename, '\t', baddumpCol, nodumpCol), + StatReportFormat.None => new Textfile(statsList, true), + StatReportFormat.Textfile => new Textfile(statsList, false), + StatReportFormat.CSV => new SeparatedValue(statsList, ','), + StatReportFormat.HTML => new Html(statsList), + StatReportFormat.SSV => new SeparatedValue(statsList, ';'), + StatReportFormat.TSV => new SeparatedValue(statsList, '\t'), _ => null, }; } /// - /// Replace the statistics that is being output + /// Create and open an output file for writing direct from a set of statistics /// - public void ReplaceStatistics(string datName, long machineCount, ItemDictionary datStats) - { - _name = datName; - _machineCount = machineCount; - _stats = datStats; - } - - /// - /// Write the report to the output stream - /// - public abstract void Write(); - - /// - /// Write out the header to the stream, if any exists - /// - public abstract void WriteHeader(); - - /// - /// Write out the mid-header to the stream, if any exists - /// - public abstract void WriteMidHeader(); - - /// - /// Write out the separator to the stream, if any exists - /// - public abstract void WriteMidSeparator(); - - /// - /// Write out the footer-separator to the stream, if any exists - /// - public abstract void WriteFooterSeparator(); - - /// - /// Write out the footer to the stream, if any exists - /// - public abstract void WriteFooter(); + /// Name of the file to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// True if the report was written correctly, false otherwise + public abstract bool WriteToFile(string outfile, bool baddumpCol, bool nodumpCol, bool throwOnError = false); /// /// Returns the human-readable file size for an arbitrary, 64-bit file size diff --git a/SabreTools.Reports/DatStatistics.cs b/SabreTools.Reports/DatStatistics.cs new file mode 100644 index 00000000..e207ade7 --- /dev/null +++ b/SabreTools.Reports/DatStatistics.cs @@ -0,0 +1,30 @@ +using SabreTools.DatFiles; + +namespace SabreTools.Reports +{ + /// + /// Statistics wrapper for outputting + /// + public class DatStatistics + { + /// + /// ItemDictionary representing the statistics + /// + public ItemDictionary Statistics { get; set; } + + /// + /// Name to display on output + /// + public string DisplayName { get; set; } + + /// + /// Total machine count to use on output + /// + public long MachineCount { get; set; } + + /// + /// Determines if statistics are for a directory or not + /// + public bool IsDirectory { get; set; } = false; + } +} diff --git a/SabreTools.Reports/Formats/Html.cs b/SabreTools.Reports/Formats/Html.cs index fb1fa520..7291385f 100644 --- a/SabreTools.Reports/Formats/Html.cs +++ b/SabreTools.Reports/Formats/Html.cs @@ -1,6 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Text; +using System.Xml; + +using SabreTools.Logging; namespace SabreTools.Reports.Formats { @@ -13,133 +18,314 @@ namespace SabreTools.Reports.Formats /// /// Create a new report from the filename /// - /// Name of the file to write out to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public Html(string filename, bool baddumpCol = false, bool nodumpCol = false) - : base(filename, baddumpCol, nodumpCol) + /// List of statistics objects to set + public Html(List statsList) + : base(statsList) { } - /// - /// Create a new report from the stream - /// - /// DatFile to write out statistics for - /// Output stream to write to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public Html(Stream stream, bool baddumpCol = false, bool nodumpCol = false) - : base(stream, baddumpCol, nodumpCol) + /// + public override bool WriteToFile(string outfile, bool baddumpCol, bool nodumpCol, bool throwOnError = false) { - } + InternalStopwatch watch = new InternalStopwatch($"Writing statistics to '{outfile}"); - /// - /// Write the report to file - /// - public override void Write() - { - string line = "\t\t\t{WebUtility.HtmlEncode(_name.Remove(0, 5))}" - : $">{WebUtility.HtmlEncode(_name)}") + "" - + $"{GetBytesReadable(_stats.TotalSize)}" - + $"{_machineCount}" - + $"{_stats.RomCount}" - + $"{_stats.DiskCount}" - + $"{_stats.CRCCount}" - + $"{_stats.MD5Count}" - + $"{_stats.SHA1Count}" - + $"{_stats.SHA256Count}" - + (_baddumpCol ? $"{_stats.BaddumpCount}" : string.Empty) - + (_nodumpCol ? $"{_stats.NodumpCount}" : string.Empty) - + "\n"; - _writer.Write(line); - _writer.Flush(); + try + { + // Try to create the output file + FileStream fs = File.Create(outfile); + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + XmlTextWriter xtw = new XmlTextWriter(fs, Encoding.UTF8) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + // Write out the header + WriteHeader(xtw, baddumpCol, nodumpCol); + + // Now process each of the statistics + for (int i = 0; i < Statistics.Count; i++) + { + // Get the current statistic + DatStatistics stat = Statistics[i]; + + // If we have a directory statistic + if (stat.IsDirectory) + { + WriteMidSeparator(xtw, baddumpCol, nodumpCol); + WriteIndividual(xtw, stat, baddumpCol, nodumpCol); + + // If we have anything but the last value, write the separator + if (i < Statistics.Count - 1) + { + WriteFooterSeparator(xtw, baddumpCol, nodumpCol); + WriteMidHeader(xtw, baddumpCol, nodumpCol); + } + } + + // If we have a normal statistic + else + { + WriteIndividual(xtw, stat, baddumpCol, nodumpCol); + } + } + + WriteFooter(xtw); + xtw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + finally + { + watch.Stop(); + } + + return true; } /// /// Write out the header to the stream, if any exists /// - public override void WriteHeader() + /// XmlTextWriter to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteHeader(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol) { - _writer.Write(@" - -
- DAT Statistics Report - -
- -

DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")

- -"); - _writer.Flush(); + xtw.WriteDocType("html", null, null, null); + xtw.WriteStartElement("html"); + + xtw.WriteStartElement("header"); + xtw.WriteElementString("title", "DAT Statistics Report"); + xtw.WriteElementString("style", @" +body { + background-color: lightgray; +} +.dir { + color: #0088FF; +}"); + xtw.WriteEndElement(); // header + + xtw.WriteStartElement("body"); + + xtw.WriteElementString("h2", $"DAT Statistics Report ({DateTime.Now.ToShortDateString()})"); + + xtw.WriteStartElement("table"); + xtw.WriteAttributeString("border", "1"); + xtw.WriteAttributeString("cellpadding", "5"); + xtw.WriteAttributeString("cellspacing", "0"); + xtw.Flush(); // Now write the mid header for those who need it - WriteMidHeader(); + WriteMidHeader(xtw, baddumpCol, nodumpCol); } /// /// Write out the mid-header to the stream, if any exists /// - public override void WriteMidHeader() + /// XmlTextWriter to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteMidHeader(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol) { - _writer.Write(@" " -+ @"" -+ (_baddumpCol ? "" : string.Empty) + (_nodumpCol ? "" : string.Empty) + "\n"); - _writer.Flush(); + xtw.WriteStartElement("tr"); + xtw.WriteAttributeString("bgcolor", "gray"); + + xtw.WriteElementString("th", "File Name"); + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Total Size"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Games"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Roms"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Disks"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("# with CRC"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("# with MD5"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("# with SHA-1"); + xtw.WriteEndElement(); // th + + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("# with SHA-256"); + xtw.WriteEndElement(); // th + + if (baddumpCol) + { + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Baddumps"); + xtw.WriteEndElement(); // th + } + + if (nodumpCol) + { + xtw.WriteStartElement("th"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString("Nodumps"); + xtw.WriteEndElement(); // th + } + + xtw.WriteEndElement(); // tr + xtw.Flush(); + } + + /// + /// Write a single set of statistics + /// + /// XmlTextWriter to write to + /// DatStatistics object to write out + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteIndividual(XmlTextWriter xtw, DatStatistics stat, bool baddumpCol, bool nodumpCol) + { + bool isDirectory = stat.DisplayName.StartsWith("DIR: "); + + xtw.WriteStartElement("tr"); + if (isDirectory) + xtw.WriteAttributeString("class", "dir"); + + xtw.WriteElementString("td", isDirectory ? WebUtility.HtmlEncode(stat.DisplayName.Remove(0, 5)) : WebUtility.HtmlEncode(stat.DisplayName)); + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(GetBytesReadable(stat.Statistics.TotalSize)); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.MachineCount.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.RomCount.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.DiskCount.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.CRCCount.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.MD5Count.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.SHA1Count.ToString()); + xtw.WriteEndElement(); // td + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.SHA256Count.ToString()); + xtw.WriteEndElement(); // td + + if (baddumpCol) + { + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.BaddumpCount.ToString()); + xtw.WriteEndElement(); // td + } + + if (nodumpCol) + { + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("align", "right"); + xtw.WriteString(stat.Statistics.NodumpCount.ToString()); + xtw.WriteEndElement(); // td + } + + xtw.WriteEndElement(); // tr + xtw.Flush(); } /// /// Write out the separator to the stream, if any exists /// - public override void WriteMidSeparator() + /// XmlTextWriter to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteMidSeparator(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol) { - _writer.Write("\n"); - _writer.Flush(); + xtw.WriteStartElement("tr"); + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("colspan", baddumpCol && nodumpCol ? "12" : (baddumpCol ^ nodumpCol ? "11" : "10")); + xtw.WriteEndElement(); // td + + xtw.WriteEndElement(); // tr + xtw.Flush(); } /// /// Write out the footer-separator to the stream, if any exists /// - public override void WriteFooterSeparator() + /// XmlTextWriter to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteFooterSeparator(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol) { - _writer.Write("\n"); - _writer.Flush(); + xtw.WriteStartElement("tr"); + xtw.WriteAttributeString("border", "0"); + + xtw.WriteStartElement("td"); + xtw.WriteAttributeString("colspan", baddumpCol && nodumpCol ? "12" : (baddumpCol ^ nodumpCol ? "11" : "10")); + xtw.WriteEndElement(); // td + + xtw.WriteEndElement(); // tr + xtw.Flush(); } /// /// Write out the footer to the stream, if any exists /// - public override void WriteFooter() + /// XmlTextWriter to write to + private void WriteFooter(XmlTextWriter xtw) { - _writer.Write(@"
File NameTotal SizeGamesRomsDisks# with CRC# with MD5# with SHA-1# with SHA-256BaddumpsNodumps
- - -"); - _writer.Flush(); + xtw.WriteEndElement(); // table + xtw.WriteEndElement(); // body + xtw.WriteEndElement(); // html + xtw.Flush(); } } } diff --git a/SabreTools.Reports/Formats/SeparatedValue.cs b/SabreTools.Reports/Formats/SeparatedValue.cs index bf3fd131..285d9ff1 100644 --- a/SabreTools.Reports/Formats/SeparatedValue.cs +++ b/SabreTools.Reports/Formats/SeparatedValue.cs @@ -1,4 +1,10 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using SabreTools.IO.Writers; +using SabreTools.Logging; namespace SabreTools.Reports.Formats { @@ -12,94 +18,142 @@ namespace SabreTools.Reports.Formats /// /// Create a new report from the filename /// - /// Name of the file to write out to + /// List of statistics objects to set /// Separator character to use in output - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public SeparatedValue(string filename, char separator, bool baddumpCol = false, bool nodumpCol = false) - : base(filename, baddumpCol, nodumpCol) + public SeparatedValue(List statsList, char separator) + : base(statsList) { _separator = separator; } - /// - /// Create a new report from the input DatFile and the stream - /// - /// Output stream to write to - /// Separator character to use in output - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public SeparatedValue(Stream stream, char separator, bool baddumpCol = false, bool nodumpCol = false) - : base(stream, baddumpCol, nodumpCol) + /// + public override bool WriteToFile(string outfile, bool baddumpCol, bool nodumpCol, bool throwOnError = false) { - _separator = separator; - } + InternalStopwatch watch = new InternalStopwatch($"Writing statistics to '{outfile}"); - /// - /// Write the report to file - /// - public override void Write() - { - string line = string.Format("\"" + _name + "\"{0}" - + "\"" + _stats.TotalSize + "\"{0}" - + "\"" + _machineCount + "\"{0}" - + "\"" + _stats.RomCount + "\"{0}" - + "\"" + _stats.DiskCount + "\"{0}" - + "\"" + _stats.CRCCount + "\"{0}" - + "\"" + _stats.MD5Count + "\"{0}" - + "\"" + _stats.SHA1Count + "\"{0}" - + "\"" + _stats.SHA256Count + "\"{0}" - + "\"" + _stats.SHA384Count + "\"{0}" - + "\"" + _stats.SHA512Count + "\"" - + (_baddumpCol ? "{0}\"" + _stats.BaddumpCount + "\"" : string.Empty) - + (_nodumpCol ? "{0}\"" + _stats.NodumpCount + "\"" : string.Empty) - + "\n", _separator); + try + { + // Try to create the output file + FileStream fs = File.Create(outfile); + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } - _writer.Write(line); - _writer.Flush(); + SeparatedValueWriter svw = new SeparatedValueWriter(fs, Encoding.UTF8) + { + Separator = _separator, + Quotes = true, + }; + + // Write out the header + WriteHeader(svw, baddumpCol, nodumpCol); + + // Now process each of the statistics + for (int i = 0; i < Statistics.Count; i++) + { + // Get the current statistic + DatStatistics stat = Statistics[i]; + + // If we have a directory statistic + if (stat.IsDirectory) + { + WriteIndividual(svw, stat, baddumpCol, nodumpCol); + + // If we have anything but the last value, write the separator + if (i < Statistics.Count - 1) + WriteFooterSeparator(svw); + } + + // If we have a normal statistic + else + { + WriteIndividual(svw, stat, baddumpCol, nodumpCol); + } + } + + svw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + finally + { + watch.Stop(); + } + + return true; } /// /// Write out the header to the stream, if any exists /// - public override void WriteHeader() + /// SeparatedValueWriter to write to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteHeader(SeparatedValueWriter svw, bool baddumpCol, bool nodumpCol) { - _writer.Write(string.Format("\"File Name\"{0}\"Total Size\"{0}\"Games\"{0}\"Roms\"{0}\"Disks\"{0}\"# with CRC\"{0}\"# with MD5\"{0}\"# with SHA-1\"{0}\"# with SHA-256\"" - + (_baddumpCol ? "{0}\"BadDumps\"" : string.Empty) + (_nodumpCol ? "{0}\"Nodumps\"" : string.Empty) + "\n", _separator)); - _writer.Flush(); + string[] headers = new string[] + { + "File Name", + "Total Size", + "Games", + "Roms", + "Disks", + "# with CRC", + "# with MD5", + "# with SHA-1", + "# with SHA-256", + "# with SHA-384", + "# with SHA-512", + baddumpCol ? "BadDumps" : string.Empty, + nodumpCol ? "Nodumps" : string.Empty, + }; + svw.WriteHeader(headers); + svw.Flush(); } /// - /// Write out the mid-header to the stream, if any exists + /// Write a single set of statistics /// - public override void WriteMidHeader() + /// SeparatedValueWriter to write to + /// DatStatistics object to write out + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + private void WriteIndividual(SeparatedValueWriter svw, DatStatistics stat, bool baddumpCol, bool nodumpCol) { - // This call is a no-op for separated value formats - } - - /// - /// Write out the separator to the stream, if any exists - /// - public override void WriteMidSeparator() - { - // This call is a no-op for separated value formats + string[] values = new string[] + { + stat.DisplayName, + stat.Statistics.TotalSize.ToString(), + stat.MachineCount.ToString(), + stat.Statistics.RomCount.ToString(), + stat.Statistics.DiskCount.ToString(), + stat.Statistics.CRCCount.ToString(), + stat.Statistics.MD5Count.ToString(), + stat.Statistics.SHA1Count.ToString(), + stat.Statistics.SHA256Count.ToString(), + stat.Statistics.SHA384Count.ToString(), + stat.Statistics.SHA512Count.ToString(), + baddumpCol ? stat.Statistics.BaddumpCount.ToString() : string.Empty, + nodumpCol ? stat.Statistics.NodumpCount.ToString() : string.Empty, + }; + svw.WriteValues(values); + svw.Flush(); } /// /// Write out the footer-separator to the stream, if any exists /// - public override void WriteFooterSeparator() + /// SeparatedValueWriter to write to + private void WriteFooterSeparator(SeparatedValueWriter svw) { - _writer.Write("\n"); - _writer.Flush(); - } - - /// - /// Write out the footer to the stream, if any exists - /// - public override void WriteFooter() - { - // This call is a no-op for separated value formats + svw.WriteString("\n"); + svw.Flush(); } } } diff --git a/SabreTools.Reports/Formats/Textfile.cs b/SabreTools.Reports/Formats/Textfile.cs index ab1abe58..f428d520 100644 --- a/SabreTools.Reports/Formats/Textfile.cs +++ b/SabreTools.Reports/Formats/Textfile.cs @@ -1,4 +1,8 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; + +using SabreTools.Logging; namespace SabreTools.Reports.Formats { @@ -7,98 +11,118 @@ namespace SabreTools.Reports.Formats ///
internal class Textfile : BaseReport { + private readonly bool _writeToConsole; + /// /// Create a new report from the filename /// - /// Name of the file to write out to - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - public Textfile(string filename, bool baddumpCol = false, bool nodumpCol = false) - : base(filename, baddumpCol, nodumpCol) + /// List of statistics objects to set + /// True to write to consoke output, false otherwise + public Textfile(List statsList, bool writeToConsole) + : base(statsList) { + _writeToConsole = writeToConsole; + } + + /// + public override bool WriteToFile(string outfile, bool baddumpCol, bool nodumpCol, bool throwOnError = false) + { + InternalStopwatch watch = new InternalStopwatch($"Writing statistics to '{outfile}"); + + try + { + // Try to create the output file + Stream fs = _writeToConsole ? Console.OpenStandardOutput() : File.Create(outfile); + if (fs == null) + { + logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new StreamWriter(fs); + + // Now process each of the statistics + for (int i = 0; i < Statistics.Count; i++) + { + // Get the current statistic + DatStatistics stat = Statistics[i]; + + // If we have a directory statistic + if (stat.IsDirectory) + { + WriteIndividual(sw, stat, baddumpCol, nodumpCol); + + // If we have anything but the last value, write the separator + if (i < Statistics.Count - 1) + WriteFooterSeparator(sw); + } + + // If we have a normal statistic + else + { + WriteIndividual(sw, stat, baddumpCol, nodumpCol); + } + } + + sw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + logger.Error(ex); + return false; + } + finally + { + watch.Stop(); + } + + return true; } /// - /// Create a new report from the stream + /// Write a single set of statistics /// - /// Output stream to write to + /// StreamWriter to write to + /// DatStatistics object to write out /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public Textfile(Stream stream, bool baddumpCol = false, bool nodumpCol = false) - : base(stream, baddumpCol, nodumpCol) + private void WriteIndividual(StreamWriter sw, DatStatistics stat, bool baddumpCol, bool nodumpCol) { - } - - /// - /// Write the report to file - /// - public override void Write() - { - string line = @"'" + _name + @"': + string line = @"'" + stat.DisplayName + @"': -------------------------------------------------- - Uncompressed size: " + GetBytesReadable(_stats.TotalSize) + @" - Games found: " + _machineCount + @" - Roms found: " + _stats.RomCount + @" - Disks found: " + _stats.DiskCount + @" - Roms with CRC: " + _stats.CRCCount + @" - Roms with MD5: " + _stats.MD5Count + @" - Roms with SHA-1: " + _stats.SHA1Count + @" - Roms with SHA-256: " + _stats.SHA256Count + @" - Roms with SHA-384: " + _stats.SHA384Count + @" - Roms with SHA-512: " + _stats.SHA512Count + "\n"; + Uncompressed size: " + GetBytesReadable(stat.Statistics.TotalSize) + @" + Games found: " + stat.MachineCount + @" + Roms found: " + stat.Statistics.RomCount + @" + Disks found: " + stat.Statistics.DiskCount + @" + Roms with CRC: " + stat.Statistics.CRCCount + @" + Roms with MD5: " + stat.Statistics.MD5Count + @" + Roms with SHA-1: " + stat.Statistics.SHA1Count + @" + Roms with SHA-256: " + stat.Statistics.SHA256Count + @" + Roms with SHA-384: " + stat.Statistics.SHA384Count + @" + Roms with SHA-512: " + stat.Statistics.SHA512Count + "\n"; - if (_baddumpCol) - line += " Roms with BadDump status: " + _stats.BaddumpCount + "\n"; + if (baddumpCol) + line += " Roms with BadDump status: " + stat.Statistics.BaddumpCount + "\n"; - if (_nodumpCol) - line += " Roms with Nodump status: " + _stats.NodumpCount + "\n"; + if (nodumpCol) + line += " Roms with Nodump status: " + stat.Statistics.NodumpCount + "\n"; // For spacing between DATs line += "\n\n"; - _writer.Write(line); - _writer.Flush(); - } - - /// - /// Write out the header to the stream, if any exists - /// - public override void WriteHeader() - { - // This call is a no-op for textfile output - } - - /// - /// Write out the mid-header to the stream, if any exists - /// - public override void WriteMidHeader() - { - // This call is a no-op for textfile output - } - - /// - /// Write out the separator to the stream, if any exists - /// - public override void WriteMidSeparator() - { - // This call is a no-op for textfile output + sw.Write(line); + sw.Flush(); } /// /// Write out the footer-separator to the stream, if any exists /// - public override void WriteFooterSeparator() + /// StreamWriter to write to + private void WriteFooterSeparator(StreamWriter sw) { - _writer.Write("\n"); - _writer.Flush(); - } - - /// - /// Write out the footer to the stream, if any exists - /// - public override void WriteFooter() - { - // This call is a no-op for textfile output + sw.Write("\n"); + sw.Flush(); } } } diff --git a/SabreTools/Features/Stats.cs b/SabreTools/Features/Stats.cs index 20a7815f..f2b87806 100644 --- a/SabreTools/Features/Stats.cs +++ b/SabreTools/Features/Stats.cs @@ -58,11 +58,11 @@ The stats that are outputted are as follows: filename = Path.GetFileName(filename); } - Statistics.OutputStats( - Inputs, + var statistics = Statistics.CalculateStatistics(Inputs, GetBoolean(features, IndividualValue)); + Statistics.Write( + statistics, filename, OutputDir, - GetBoolean(features, IndividualValue), GetBoolean(features, BaddumpColumnValue), GetBoolean(features, NodumpColumnValue), GetStatReportFormat(features));