using System; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using FSI.Lib.Tools.RoboSharp.Interfaces; using FSI.Lib.Tools.RoboSharp.EventArgObjects; namespace FSI.Lib.Tools.RoboSharp.Results { /// /// Information about number of items Copied, Skipped, Failed, etc. /// /// will not typically raise any events, but this object is used for other items, such as and to present results whose values may update periodically. /// /// /// /// public class Statistic : IStatistic { internal static IStatistic Default_Bytes = new Statistic(type: StatType.Bytes); internal static IStatistic Default_Files = new Statistic(type: StatType.Files); internal static IStatistic Default_Dirs = new Statistic(type: StatType.Directories); #region < Constructors > /// Create a new Statistic object of [Obsolete("Statistic Types require Initialization with a StatType")] private Statistic() { } /// Create a new Statistic object public Statistic(StatType type) { Type = type; } /// Create a new Statistic object public Statistic(StatType type, string name) { Type = type; Name = name; } /// Clone an existing Statistic object public Statistic(Statistic stat) { Type = stat.Type; NameField = stat.Name; TotalField = stat.Total; CopiedField = stat.Copied; SkippedField = stat.Skipped; MismatchField = stat.Mismatch; FailedField = stat.Failed; ExtrasField = stat.Extras; } /// Clone an existing Statistic object internal Statistic(StatType type, string name, long total, long copied, long skipped, long mismatch, long failed, long extras) { Type = type; NameField = name; TotalField = total; CopiedField = copied; SkippedField = skipped; MismatchField = mismatch; FailedField = failed; ExtrasField = extras; } /// Describe the Type of Statistics Object public enum StatType { /// Statistics object represents count of Directories Directories, /// Statistics object represents count of Files Files, /// Statistics object represents a Size ( number of bytes ) Bytes } #endregion #region < Fields > private string NameField = ""; private long TotalField = 0; private long CopiedField = 0; private long SkippedField = 0; private long MismatchField = 0; private long FailedField = 0; private long ExtrasField = 0; #endregion #region < Events > /// This toggle Enables/Disables firing the Event to avoid firing it when doing multiple consecutive changes to the values internal bool EnablePropertyChangeEvent = true; private bool PropertyChangedListener => EnablePropertyChangeEvent && PropertyChanged != null; private bool Listener_TotalChanged => EnablePropertyChangeEvent && OnTotalChanged != null; private bool Listener_CopiedChanged => EnablePropertyChangeEvent && OnCopiedChanged != null; private bool Listener_SkippedChanged => EnablePropertyChangeEvent && OnSkippedChanged != null; private bool Listener_MismatchChanged => EnablePropertyChangeEvent && OnMisMatchChanged != null; private bool Listener_FailedChanged => EnablePropertyChangeEvent && OnFailedChanged != null; private bool Listener_ExtrasChanged => EnablePropertyChangeEvent && OnExtrasChanged != null; /// /// This event will fire when the value of the statistic is updated via Adding / Subtracting methods.
/// Provides object. ///
/// /// Allows use with both binding to controls and binding.
/// EventArgs can be passed into after casting. ///
public event PropertyChangedEventHandler PropertyChanged; /// Handles any value changes public delegate void StatChangedHandler(Statistic sender, StatChangedEventArg e); /// Occurs when the Property is updated. public event StatChangedHandler OnTotalChanged; /// Occurs when the Property is updated. public event StatChangedHandler OnCopiedChanged; /// Occurs when the Property is updated. public event StatChangedHandler OnSkippedChanged; /// Occurs when the Property is updated. public event StatChangedHandler OnMisMatchChanged; /// Occurs when the Property is updated. public event StatChangedHandler OnFailedChanged; /// Occurs when the Property is updated. public event StatChangedHandler OnExtrasChanged; #endregion #region < Properties > /// /// Checks all values and determines if any of them are != 0. /// public bool NonZeroValue => TotalField != 0 || CopiedField != 0 || SkippedField != 0 || MismatchField != 0 || FailedField != 0 || ExtrasField != 0; /// /// Name of the Statistics Object /// public string Name { get => NameField; set { if (value != NameField) { NameField = value ?? ""; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name")); } } } /// /// /// public StatType Type { get; } /// public long Total { get => TotalField; internal set { if (TotalField != value) { var e = PrepEventArgs(TotalField, value, "Total"); TotalField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnTotalChanged?.Invoke(this, e.Value.Item1); } } } } /// public long Copied { get => CopiedField; internal set { if (CopiedField != value) { var e = PrepEventArgs(CopiedField, value, "Copied"); CopiedField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnCopiedChanged?.Invoke(this, e.Value.Item1); } } } } /// public long Skipped { get => SkippedField; internal set { if (SkippedField != value) { var e = PrepEventArgs(SkippedField, value, "Skipped"); SkippedField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnSkippedChanged?.Invoke(this, e.Value.Item1); } } } } /// public long Mismatch { get => MismatchField; internal set { if (MismatchField != value) { var e = PrepEventArgs(MismatchField, value, "Mismatch"); MismatchField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnMisMatchChanged?.Invoke(this, e.Value.Item1); } } } } /// public long Failed { get => FailedField; internal set { if (FailedField != value) { var e = PrepEventArgs(FailedField, value, "Failed"); FailedField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnFailedChanged?.Invoke(this, e.Value.Item1); } } } } /// public long Extras { get => ExtrasField; internal set { if (ExtrasField != value) { var e = PrepEventArgs(ExtrasField, value, "Extras"); ExtrasField = value; if (e != null) { PropertyChanged?.Invoke(this, e.Value.Item2); OnExtrasChanged?.Invoke(this, e.Value.Item1); } } } } #endregion #region < ToString Methods > /// /// Returns a string that represents the current object. /// public override string ToString() => ToString(false, true, ", "); /// /// Customize the returned string /// /// Include string representation of /// Include "Total:" / "Copied:" / etc in the string to identify the values /// Value Delimieter /// /// Include the delimiter after the 'Type' - Only used if us also true.
/// When is true, a space always exist after the type string. This would add delimiter instead of the space. /// /// /// TRUE, TRUE, "," --> $"{Type} Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" /// FALSE, TRUE, "," --> $"Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" /// FALSE, FALSE, "," --> $"{Total}, {Copied}, {Skipped}, {Mismatch}, {Failed}, {Extras}" /// public string ToString(bool IncludeType, bool IncludePrefix, string Delimiter, bool DelimiterAfterType = false) { return $"{ToString_Type(IncludeType, DelimiterAfterType)}" + $"{(IncludeType && DelimiterAfterType ? Delimiter : "")}" + $"{ToString_Total(false, IncludePrefix)}{Delimiter}" + $"{ToString_Copied(false, IncludePrefix)}{Delimiter}" + $"{ToString_Skipped(false, IncludePrefix)}{Delimiter}" + $"{ToString_Mismatch(false, IncludePrefix)}{Delimiter}" + $"{ToString_Failed(false, IncludePrefix)}{Delimiter}" + $"{ToString_Extras(false, IncludePrefix)}"; } /// Get the as a string public string ToString_Type() => ToString_Type(true).Trim(); private string ToString_Type(bool IncludeType, bool Trim = false) => IncludeType ? $"{Type}{(Trim? "" : " ")}" : ""; /// Get the string describing the /// /// public string ToString_Total(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix? "Total: " : "")}{Total}"; /// Get the string describing the /// public string ToString_Copied(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Copied: " : "")}{Copied}"; /// Get the string describing the /// public string ToString_Extras(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Extras: " : "")}{Extras}"; /// Get the string describing the /// public string ToString_Failed(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Failed: " : "")}{Failed}"; /// Get the string describing the /// public string ToString_Mismatch(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Mismatch: " : "")}{Mismatch}"; /// Get the string describing the /// public string ToString_Skipped(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Skipped: " : "")}{Skipped}"; #endregion #region < Parsing Methods > /// /// Parse a string and for the tokens reported by RoboCopy /// /// Statistic Type to produce /// LogLine produced by RoboCopy in Summary Section /// New Statistic Object public static Statistic Parse(StatType type, string line) { var res = new Statistic(type); var tokenNames = new[] { nameof(Total), nameof(Copied), nameof(Skipped), nameof(Mismatch), nameof(Failed), nameof(Extras) }; var patternBuilder = new StringBuilder(@"^.*:"); foreach (var tokenName in tokenNames) { var tokenPattern = GetTokenPattern(tokenName); patternBuilder.Append(@"\s+").Append(tokenPattern); } var pattern = patternBuilder.ToString(); var match = Regex.Match(line, pattern); if (!match.Success) return res; var props = res.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (var tokenName in tokenNames) { var prop = props.FirstOrDefault(x => x.Name == tokenName); if (prop == null) continue; var tokenString = match.Groups[tokenName].Value; var tokenValue = ParseTokenString(tokenString); prop.SetValue(res, tokenValue, null); } return res; } private static string GetTokenPattern(string tokenName) { return $@"(?<{tokenName}>[\d\.]+(\s\w)?)"; } private static long ParseTokenString(string tokenString) { if (string.IsNullOrWhiteSpace(tokenString)) return 0; tokenString = tokenString.Trim(); if (Regex.IsMatch(tokenString, @"^\d+$", RegexOptions.Compiled)) return long.Parse(tokenString); var match = Regex.Match(tokenString, @"(?[\d\.,]+)(\.(?\d+))\s(?\w)", RegexOptions.Compiled); if (match.Success) { var mains = match.Groups["Mains"].Value.Replace(".", "").Replace(",", ""); var fraction = match.Groups["Fraction"].Value; var unit = match.Groups["Unit"].Value.ToLower(); var number = double.Parse($"{mains}.{fraction}", NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); switch (unit) { case "k": // Byte = kBytes * 1024 number *= Math.Pow(1024, 1); break; case "m": // Byte = MBytes * 1024 * 1024 number *= Math.Pow(1024, 2); break; case "g": // Byte = GBytes * 1024 * 1024 * 1024 number *= Math.Pow(1024, 3); break; case "t": // Byte = TBytes * 1024 * 1024 * 1024 * 1024 number *= Math.Pow(1024, 4); break; } return Convert.ToInt64(number); } return 0; } #endregion #region < Reset Method > /// /// Set the values for this object to 0 /// public void Reset(bool enablePropertyChangeEvent) { EnablePropertyChangeEvent = enablePropertyChangeEvent; Reset(); EnablePropertyChangeEvent = true; } /// /// Reset all values to Zero ( 0 ) -- Used by for the properties /// [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] public void Reset() { Statistic OriginalValues = PropertyChangedListener && NonZeroValue ? this.Clone() : null; //Total var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, TotalField, 0, "Total"); TotalField = 0; //Copied var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, CopiedField, 0, "Copied"); CopiedField = 0; //Extras var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, ExtrasField, 0, "Extras"); ExtrasField = 0; //Failed var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, FailedField, 0, "Failed"); FailedField = 0; //Mismatch var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, MismatchField, 0, "Mismatch"); MismatchField = 0; //Skipped var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, SkippedField, 0, "Skipped"); SkippedField = 0; //Trigger Events TriggerDeferredEvents(OriginalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped); } #endregion #region < Trigger Deferred Events > /// /// Prep Event Args for SETTERS of the properties /// [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] private Lazy> PrepEventArgs(long OldValue, long NewValue, string PropertyName) { if (!EnablePropertyChangeEvent) return null; var old = this.Clone(); return new Lazy>(() => { StatChangedEventArg e1 = new StatChangedEventArg(this, OldValue, NewValue, PropertyName); var e2 = new StatisticPropertyChangedEventArgs(this, old, PropertyName); return new Tuple(e1, e2); }); } /// /// Prep event args for the ADD and RESET methods /// [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] private StatChangedEventArg ConditionalPrepEventArgs(bool Listener, long OldValue, long NewValue, string PropertyName)=> !Listener || OldValue == NewValue ? null : new StatChangedEventArg(this, OldValue, NewValue, PropertyName); /// /// Raises the events that were deferred while item was object was still being calculated by ADD / RESET /// [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] private void TriggerDeferredEvents(Statistic OriginalValues, StatChangedEventArg eTotal, StatChangedEventArg eCopied, StatChangedEventArg eExtras, StatChangedEventArg eFailed, StatChangedEventArg eMismatch, StatChangedEventArg eSkipped) { //Perform Events int i = 0; if (eTotal != null) { i = 1; OnTotalChanged?.Invoke(this, eTotal); } if (eCopied != null) { i += 2; OnCopiedChanged?.Invoke(this, eCopied); } if (eExtras != null) { i += 4; OnExtrasChanged?.Invoke(this, eExtras); } if (eFailed != null) { i += 8; OnFailedChanged?.Invoke(this, eFailed); } if (eMismatch != null) { i += 16; OnMisMatchChanged?.Invoke(this, eMismatch); } if (eSkipped != null) { i += 32; OnSkippedChanged?.Invoke(this, eSkipped); } //Trigger PropertyChangeEvent if (OriginalValues != null) { switch (i) { case 1: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eTotal.PropertyName)); return; case 2: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eCopied.PropertyName)); return; case 4: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eExtras.PropertyName)); return; case 8: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eFailed.PropertyName)); return; case 16: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eMismatch.PropertyName)); return; case 32: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eSkipped.PropertyName)); return; default: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, String.Empty)); return; } } } #endregion #region < ADD Methods > /// /// Add the supplied values to this Statistic object.
/// Events are defered until all the fields have been added together. ///
/// /// /// /// /// /// [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] public void Add(long total = 0, long copied = 0, long extras = 0, long failed = 0, long mismatch = 0, long skipped = 0) { //Store the original object values for the event args Statistic originalValues = PropertyChangedListener ? this.Clone() : null; //Total long i = TotalField; TotalField += total; var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, i, TotalField, "Total"); //Copied i = CopiedField; CopiedField += copied; var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, i, CopiedField, "Copied"); //Extras i = ExtrasField; ExtrasField += extras; var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, i, ExtrasField, "Extras"); //Failed i = FailedField; FailedField += failed; var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, i, FailedField, "Failed"); //Mismatch i = MismatchField; MismatchField += mismatch; var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, i, MismatchField, "Mismatch"); //Skipped i = SkippedField; SkippedField += skipped; var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, i, SkippedField, "Skipped"); //Trigger Events if (EnablePropertyChangeEvent) TriggerDeferredEvents(originalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped); } /// /// Add the results of the supplied Statistics object to this Statistics object.
/// Events are defered until all the fields have been added together. ///
/// Statistics Item to add [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] public void AddStatistic(IStatistic stat) { if (stat != null && stat.Type == this.Type && stat.NonZeroValue) Add(stat.Total, stat.Copied, stat.Extras, stat.Failed, stat.Mismatch, stat.Skipped); } #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) /// /// internal void AddStatistic(IStatistic stats, bool enablePropertyChangedEvent) { EnablePropertyChangeEvent = enablePropertyChangedEvent; AddStatistic(stats); EnablePropertyChangeEvent = true; } #pragma warning restore CS1573 /// /// Add the results of the supplied Statistics objects to this Statistics object. /// /// Statistics Item to add public void AddStatistic(IEnumerable stats) { foreach (Statistic stat in stats) { EnablePropertyChangeEvent = stat == stats.Last(); AddStatistic(stat); } } /// /// Adds to the appropriate property based on the 'PropertyChanged' value.
/// Will only add the value if the == . ///
/// Arg provided by either or a Statistic's object's On*Changed events public void AddStatistic(PropertyChangedEventArgs eventArgs) { //Only process the args if of the proper type var e = eventArgs as IStatisticPropertyChangedEventArg; if (e == null) return; if (e.StatType != this.Type || (e.Is_StatisticPropertyChangedEventArgs && e.Is_StatChangedEventArg)) { // INVALID! } else if (e.Is_StatisticPropertyChangedEventArgs) { var e1 = (StatisticPropertyChangedEventArgs)eventArgs; AddStatistic(e1.Difference); } else if (e.Is_StatChangedEventArg) { var e2 = (StatChangedEventArg)eventArgs; switch (e.PropertyName) { case "": //String.Empty means all fields have changed AddStatistic(e2.Sender); break; case "Copied": this.Copied += e2.Difference; break; case "Extras": this.Extras += e2.Difference; break; case "Failed": this.Failed += e2.Difference; break; case "Mismatch": this.Mismatch += e2.Difference; break; case "Skipped": this.Skipped += e2.Difference; break; case "Total": this.Total += e2.Difference; break; } } } /// /// Combine the results of the supplied statistics objects of the specified type. /// /// Collection of objects /// Create a new Statistic object of this type. /// New Statistics Object public static Statistic AddStatistics(IEnumerable stats, StatType statType) { Statistic ret = new Statistic(statType); ret.AddStatistic(stats.Where(s => s.Type == statType) ); return ret; } #endregion ADD #region < AVERAGE Methods > /// /// Combine the supplied objects, then get the average. /// /// Array of Stats objects public void AverageStatistic(IEnumerable stats) { this.AddStatistic(stats); int cnt = stats.Count() + 1; Total /= cnt; Copied /= cnt; Extras /= cnt; Failed /= cnt; Mismatch /= cnt; Skipped /= cnt; } /// New Statistics Object /// public static Statistic AverageStatistics(IEnumerable stats, StatType statType) { Statistic stat = AddStatistics(stats, statType); int cnt = stats.Count(s => s.Type == statType); if (cnt > 1) { stat.Total /= cnt; stat.Copied /= cnt; stat.Extras /= cnt; stat.Failed /= cnt; stat.Mismatch /= cnt; stat.Skipped /= cnt; } return stat; } #endregion AVERAGE #region < Subtract Methods > /// /// Subtract Method used by
/// Events are deferred until all value changes have completed. ///
/// Statistics Item to subtract #if !NET40 [MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)] #endif public void Subtract(IStatistic stat) { if (stat.Type == this.Type && stat.NonZeroValue) Add(-stat.Total, -stat.Copied, -stat.Extras, -stat.Failed, -stat.Mismatch, -stat.Skipped); } #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do) /// /// internal void Subtract(IStatistic stats, bool enablePropertyChangedEvent) { EnablePropertyChangeEvent = enablePropertyChangedEvent; Subtract(stats); EnablePropertyChangeEvent = true; } #pragma warning restore CS1573 /// /// Subtract the results of the supplied Statistics objects to this Statistics object. /// /// Statistics Item to subtract public void Subtract(IEnumerable stats) { foreach (Statistic stat in stats) { EnablePropertyChangeEvent = stat == stats.Last(); Subtract(stat); } } /// Statistics object to clone /// /// Clone of the object with the subtracted from it. /// public static Statistic Subtract(IStatistic MainStat, IStatistic stats) { var ret = MainStat.Clone(); ret.Subtract(stats); return ret; } #endregion Subtract /// public Statistic Clone() => new Statistic(this); object ICloneable.Clone() => new Statistic(this); } }