diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..65ec3d2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.0.0] - 2018-04-23 +### Added + - Adding extension method for getting Informational Version from an Assembly + - Adding WeakKeyDictionary for caching + - Reflection util methods + +### Changed + - JSON serialization/deserialization features moved into base converter + - Removed suffix from assembly name + \ No newline at end of file diff --git a/CHANGELOG.txt b/CHANGELOG.txt deleted file mode 100644 index 825f45d..0000000 --- a/CHANGELOG.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). diff --git a/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs new file mode 100644 index 0000000..21c7874 --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/WeakKeyDictionaryTest.cs @@ -0,0 +1,158 @@ +using System; +using ICD.Common.Utils.Collections; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public sealed class WeakKeyDictionaryTest + { + private sealed class TestClass + { + } + + #region Properties + + [Test] + public void CountTest() + { +#if DEBUG + Assert.Inconclusive(); + return; +#endif + WeakKeyDictionary dict = new WeakKeyDictionary(); + Assert.AreEqual(0, dict.Count); + + TestClass instance = new TestClass(); + + dict.Add(instance, 0); + + Assert.AreEqual(1, dict.Count, "Expected the added item to increase the dictionary size"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.AreEqual(1, dict.Count, "Expected the dictionary to have one uncollected item"); + + // Need to actually USE the instance at some point AFTER the collection, otherwise it gets optimized out + // and is collected prematurely. + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + instance.ToString(); + + // Now clear the reference to make sure the instance gets collected. + // ReSharper disable once RedundantAssignment + instance = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.AreEqual(0, dict.Count, "Expected the dictionary to be empty after collecting"); + } + + [Test] + public void KeysTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ValuesTest() + { + Assert.Inconclusive(); + } + + [Test] + public void IndexerTest() + { + Assert.Inconclusive(); + } + +#endregion + +#region Methods + + [Test] + public void AddTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ContainsKeyTest() + { + Assert.Inconclusive(); + } + + [Test] + public void RemoveTest() + { + Assert.Inconclusive(); + } + + [Test] + public void TryGetValueTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ClearTest() + { + Assert.Inconclusive(); + } + + [Test] + public void RemoveCollectedEntries() + { +#if DEBUG + Assert.Inconclusive(); + return; +#endif + WeakKeyDictionary dict = new WeakKeyDictionary(); + + dict.RemoveCollectedEntries(); + Assert.AreEqual(0, dict.Count, "Expected new dictionary to start empty"); + + TestClass instance = new TestClass(); + + dict.Add(instance, 0); + dict.RemoveCollectedEntries(); + + Assert.AreEqual(1, dict.Count, "Expected instance to add to the dictionary count"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + dict.RemoveCollectedEntries(); + + Assert.AreEqual(1, dict.Count, "Expected instance to remain uncollected and stay in the dictionary"); + + // Need to actually USE the instance at some point AFTER the collection, otherwise it gets optimized out + // and is collected prematurely. + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + instance.ToString(); + + // Now clear the reference to make sure the instance gets collected. + // ReSharper disable once RedundantAssignment + instance = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + dict.RemoveCollectedEntries(); + + Assert.AreEqual(0, dict.Count, "Expected instance to be collected and removed from the dictionary"); + } + + [Test] + public void GetEnumeratorTest() + { + Assert.Inconclusive(); + } + +#endregion + } +} diff --git a/ICD.Common.Utils/AttributeUtils.cs b/ICD.Common.Utils/AttributeUtils.cs index e9cf173..637b8cc 100644 --- a/ICD.Common.Utils/AttributeUtils.cs +++ b/ICD.Common.Utils/AttributeUtils.cs @@ -84,6 +84,14 @@ namespace ICD.Common.Utils { foreach (Exception inner in e.LoaderExceptions) { + if (inner is System.IO.FileNotFoundException) + { + Logger.AddEntry(eSeverity.Error, + "{0} failed to cache assembly {1} - Could not find one or more dependencies by path", + typeof(AttributeUtils).Name, assembly.GetName().Name); + continue; + } + Logger.AddEntry(eSeverity.Error, inner, "{0} failed to cache assembly {1}", typeof(AttributeUtils).Name, assembly.GetName().Name); } diff --git a/ICD.Common.Utils/Collections/WeakKeyDictionary.cs b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs new file mode 100644 index 0000000..402718c --- /dev/null +++ b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Properties; +using ICD.Common.Utils.Extensions; + +namespace ICD.Common.Utils.Collections +{ + public sealed class WeakKeyReference : WeakReference + { + private readonly int m_HashCode; + + public new T Target { get { return (T)base.Target; } } + + public int HashCode { get { return m_HashCode; } } + + /// + /// Constructor. + /// + /// + public WeakKeyReference(T key) + : this(key, EqualityComparer.Default) + { + } + + /// + /// Constructor. + /// + /// + /// + public WeakKeyReference(T key, IEqualityComparer comparer) + : base(key) + { + if (comparer == null) + throw new ArgumentNullException("comparer"); + + // Retain the object's hash code immediately so that even + // if the target is GC'ed we will be able to find and + // remove the dead weak reference. + m_HashCode = comparer.GetHashCode(key); + } + } + + public sealed class WeakKeyComparer : IEqualityComparer> + { + private readonly IEqualityComparer m_Comparer; + + /// + /// Constructor. + /// + /// + public WeakKeyComparer(IEqualityComparer comparer) + { + if (comparer == null) + comparer = EqualityComparer.Default; + + m_Comparer = comparer; + } + + public int GetHashCode(WeakKeyReference weakKey) + { + return weakKey.HashCode; + } + + // Note: There are actually 9 cases to handle here. + // + // Let Wa = Alive Weak Reference + // Let Wd = Dead Weak Reference + // Let S = Strong Reference + // + // x | y | Equals(x,y) + // ------------------------------------------------- + // Wa | Wa | comparer.Equals(x.Target, y.Target) + // Wa | Wd | false + // Wa | S | comparer.Equals(x.Target, y) + // Wd | Wa | false + // Wd | Wd | x == y + // Wd | S | false + // S | Wa | comparer.Equals(x, y.Target) + // S | Wd | false + // S | S | comparer.Equals(x, y) + // ------------------------------------------------- + public bool Equals(WeakKeyReference x, WeakKeyReference y) + { + bool xIsDead; + bool yIsDead; + + T first = GetTarget(x, out xIsDead); + T second = GetTarget(y, out yIsDead); + + if (xIsDead) + return yIsDead && x == y; + + return !yIsDead && m_Comparer.Equals(first, second); + } + + private static T GetTarget(WeakKeyReference obj, out bool isDead) + { + T target = obj.Target; + isDead = !obj.IsAlive; + return target; + } + } + + /// + /// WeakDictionary keeps weak references to keys and drops key/value pairs once the key is garbage collected. + /// + /// + /// + public sealed class WeakKeyDictionary : IDictionary + { + private readonly Dictionary, TValue> m_Dictionary; + private readonly IEqualityComparer m_Comparer; + + #region Properties + + public int Count { get { return GetAliveKvps().Count(); } } + + public bool IsReadOnly { get { return false; } } + + public ICollection Keys { get { return GetAliveKvps().Select(kvp => kvp.Key).ToArray(); } } + + public ICollection Values { get { return GetAliveKvps().Select(kvp => kvp.Value).ToArray(); } } + + public TValue this[TKey key] + { + get + { + TValue output; + if (TryGetValue(key, out output)) + return output; + + throw new KeyNotFoundException(); + } + set + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary[weakKey] = value; + } + } + + #endregion + + public WeakKeyDictionary() + : this(0) + { + } + + public WeakKeyDictionary(int capacity) + : this(capacity, EqualityComparer.Default) + { + } + + public WeakKeyDictionary(IEqualityComparer comparer) + : this(0, comparer) + { + } + + public WeakKeyDictionary(int capacity, IEqualityComparer comparer) + { + m_Comparer = comparer; + + WeakKeyComparer keyComparer = new WeakKeyComparer(m_Comparer); + m_Dictionary = new Dictionary, TValue>(capacity, keyComparer); + } + + #region Methods + + public void Add(TKey key, TValue value) + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary.Add(weakKey, value); + } + + public bool ContainsKey(TKey key) + { + TValue unused; + return TryGetValue(key, out unused); + } + + public bool Remove(TKey key) + { + return m_Dictionary.Remove(new WeakKeyReference(key, m_Comparer)); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return m_Dictionary.TryGetValue(new WeakKeyReference(key, m_Comparer), out value); + } + + public void Clear() + { + m_Dictionary.Clear(); + } + + /// + /// Removes the left-over weak references for entries in the dictionary + /// whose key or value has already been reclaimed by the garbage + /// collector. This will reduce the dictionary's Count by the number + /// of dead key-value pairs that were eliminated. + /// + [PublicAPI] + public void RemoveCollectedEntries() + { + IEnumerable> toRemove = + m_Dictionary.Select(pair => pair.Key) + .Where(weakKey => !weakKey.IsAlive) + .ToArray(); + + m_Dictionary.RemoveAll(toRemove); + } + + #region ICollection + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + GetAliveKvps().CopyTo(array, arrayIndex); + } + + private IEnumerable> GetAliveKvps() + { + foreach (KeyValuePair, TValue> kvp in m_Dictionary) + { + WeakKeyReference weakKey = kvp.Key; + TKey key = weakKey.Target; + if (weakKey.IsAlive) + yield return new KeyValuePair(key, kvp.Value); + } + } + + #endregion + + #region IEnumerator + + public IEnumerator> GetEnumerator() + { + return GetAliveKvps().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #endregion + } +} diff --git a/ICD.Common.Utils/EnumUtils.cs b/ICD.Common.Utils/EnumUtils.cs index 03b6b90..af3f1e9 100644 --- a/ICD.Common.Utils/EnumUtils.cs +++ b/ICD.Common.Utils/EnumUtils.cs @@ -321,7 +321,7 @@ namespace ICD.Common.Utils throw new ArgumentException(string.Format("{0} is not an enum", value == null ? "NULL" : value.GetType().Name), "value"); int maxEnumValue = (GetValues().Max(v => (int)(object)v) * 2) -1 ; - return Enumerable.Range(1, maxEnumValue).Cast().Where(v => HasFlags(value, v)); + return Enumerable.Range(1, maxEnumValue).Select(i => (T)(object)i ).Where(v => HasFlags(value, v)); } /// diff --git a/ICD.Common.Utils/EventArguments/GenericEventArgs.cs b/ICD.Common.Utils/EventArguments/GenericEventArgs.cs index 895a067..9ed83a6 100644 --- a/ICD.Common.Utils/EventArguments/GenericEventArgs.cs +++ b/ICD.Common.Utils/EventArguments/GenericEventArgs.cs @@ -2,8 +2,11 @@ namespace ICD.Common.Utils.EventArguments { - public class GenericEventArgs : EventArgs + public class GenericEventArgs : EventArgs, IGenericEventArgs { + /// + /// Gets the wrapped data associated with the event. + /// public T Data { get; private set; } /// diff --git a/ICD.Common.Utils/EventArguments/IGenericEventArgs.cs b/ICD.Common.Utils/EventArguments/IGenericEventArgs.cs new file mode 100644 index 0000000..626a292 --- /dev/null +++ b/ICD.Common.Utils/EventArguments/IGenericEventArgs.cs @@ -0,0 +1,10 @@ +namespace ICD.Common.Utils.EventArguments +{ + public interface IGenericEventArgs + { + /// + /// Gets the wrapped data associated with the event. + /// + T Data { get; } + } +} diff --git a/ICD.Common.Utils/Extensions/AssemblyExtensions.cs b/ICD.Common.Utils/Extensions/AssemblyExtensions.cs index 3a67c37..fafd5a9 100644 --- a/ICD.Common.Utils/Extensions/AssemblyExtensions.cs +++ b/ICD.Common.Utils/Extensions/AssemblyExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using ICD.Common.Properties; using ICD.Common.Utils.IO; #if SIMPLSHARP @@ -12,7 +13,7 @@ namespace ICD.Common.Utils.Extensions public static class AssemblyExtensions { /// - /// Gets the path for the given assembly. Returns null if the assembly can not be found on disk. + /// Gets the path for the assembly. Returns null if the assembly can not be found on disk. /// /// /// @@ -45,10 +46,11 @@ namespace ICD.Common.Utils.Extensions } /// - /// Gets the creation date of the given assembly. + /// Gets the creation date of the assembly. /// /// /// + [PublicAPI] public static DateTime GetCreationTime(this Assembly extends) { if (extends == null) @@ -57,5 +59,40 @@ namespace ICD.Common.Utils.Extensions string path = extends.GetPath(); return path == null ? DateTime.MinValue : IcdFile.GetCreationTime(path); } + + /// + /// Gets the informational version for the assembly. + /// + /// + /// + [PublicAPI] + public static string GetInformationalVersion(this Assembly extends) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + string version; + if (extends.TryGetInformationalVersion(out version)) + return version; + + throw new InvalidOperationException("Assembly has no informational version attribute."); + } + + /// + /// Tries to get the informational version for the assembly. + /// + /// + /// + /// + [PublicAPI] + public static bool TryGetInformationalVersion(this Assembly extends, out string version) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + return extends.GetCustomAttributes() + .Select(a => a.InformationalVersion) + .TryFirst(out version); + } } } diff --git a/ICD.Common.Utils/Extensions/BoolExtensions.cs b/ICD.Common.Utils/Extensions/BoolExtensions.cs new file mode 100644 index 0000000..b820422 --- /dev/null +++ b/ICD.Common.Utils/Extensions/BoolExtensions.cs @@ -0,0 +1,10 @@ +namespace ICD.Common.Utils.Extensions +{ + public static class BoolExtensions + { + public static ushort ToUShort(this bool b) + { + return b ? (ushort)1 : (ushort)0; + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs index a6662f4..765d113 100644 --- a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs +++ b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs @@ -578,6 +578,23 @@ namespace ICD.Common.Utils.Extensions return extends.Where(i => !comparer.Equals(item, i)); } + /// + /// Copies all the elements of the current one-dimensional array to the specified one-dimensional array + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// + /// + /// + /// + public static void CopyTo(this IEnumerable extends, T[] array, int index) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + ICollection collection = extends as ICollection ?? extends.ToArray(); + collection.CopyTo(array, index); + } + /// /// Returns the sequence as a IcdHashSet. /// diff --git a/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs b/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs index 958cf38..6e222db 100644 --- a/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs +++ b/ICD.Common.Utils/Extensions/EventHandlerExtensions.cs @@ -28,6 +28,9 @@ namespace ICD.Common.Utils.Extensions public static void Raise(this EventHandler extends, object sender, T args) where T : EventArgs { + if (args == null) + throw new ArgumentNullException("args"); + if (extends != null) extends(sender, args); } diff --git a/ICD.Common.Utils/Extensions/JsonExtensions.cs b/ICD.Common.Utils/Extensions/JsonExtensions.cs index 77a0eb0..ac56fa2 100644 --- a/ICD.Common.Utils/Extensions/JsonExtensions.cs +++ b/ICD.Common.Utils/Extensions/JsonExtensions.cs @@ -75,6 +75,24 @@ namespace ICD.Common.Utils.Extensions return Type.GetType(value); } + /// + /// Gets the current value as an unsigned integer. + /// + /// + /// + [PublicAPI] + public static uint GetValueAsUInt(this JsonReader extends) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (extends.TokenType == JsonToken.Integer) + return (uint)(long)extends.Value; + + string message = string.Format("Token {0} {1} is not {2}", extends.TokenType, extends.Value, JsonToken.Integer); + throw new InvalidCastException(message); + } + /// /// Gets the current value as an integer. /// diff --git a/ICD.Common.Utils/Extensions/ListExtensions.cs b/ICD.Common.Utils/Extensions/ListExtensions.cs index 9e72772..50c28c7 100644 --- a/ICD.Common.Utils/Extensions/ListExtensions.cs +++ b/ICD.Common.Utils/Extensions/ListExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using ICD.Common.Properties; +using ICD.Common.Utils.Comparers; namespace ICD.Common.Utils.Extensions { @@ -10,6 +11,70 @@ namespace ICD.Common.Utils.Extensions /// public static class ListExtensions { + /// + /// Adds the items into a sorted list. + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, IEnumerable items) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (items == null) + throw new ArgumentNullException("items"); + + extends.AddSorted(items, Comparer.Default); + } + + /// + /// Adds the items into a sorted list. + /// + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, IEnumerable items, IComparer comparer) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (items == null) + throw new ArgumentNullException("items"); + + if (comparer == null) + throw new ArgumentNullException("comparer"); + + items.ForEach(i => extends.AddSorted(i, comparer)); + } + + /// + /// Adds the items into a sorted list. + /// + /// + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, IEnumerable items, Func predicate) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (items == null) + throw new ArgumentNullException("items"); + + if (predicate == null) + throw new ArgumentNullException("predicate"); + + PredicateComparer comparer = new PredicateComparer(predicate); + extends.AddSorted(items, comparer); + } + /// /// Adds the item into a sorted list. /// @@ -66,6 +131,27 @@ namespace ICD.Common.Utils.Extensions extends.Insert(index, item); } + /// + /// Adds the item into a sorted list. + /// + /// + /// + /// + /// + /// + [PublicAPI] + public static void AddSorted(this List extends, T item, Func predicate) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (predicate == null) + throw new ArgumentNullException("predicate"); + + PredicateComparer comparer = new PredicateComparer(predicate); + extends.AddSorted(item, comparer); + } + /// /// Pads the list to the given total length. /// diff --git a/ICD.Common.Utils/Extensions/UshortExtensions.cs b/ICD.Common.Utils/Extensions/UshortExtensions.cs new file mode 100644 index 0000000..a4bae69 --- /dev/null +++ b/ICD.Common.Utils/Extensions/UshortExtensions.cs @@ -0,0 +1,10 @@ +namespace ICD.Common.Utils.Extensions +{ + public static class UShortExtensions + { + public static bool ToBool(this ushort u) + { + return u != 0; + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 63af157..231f2aa 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -74,6 +74,7 @@ + @@ -81,14 +82,17 @@ + + + diff --git a/ICD.Common.Utils/IO/IcdPath.cs b/ICD.Common.Utils/IO/IcdPath.cs index 37d0bad..6ee4a52 100644 --- a/ICD.Common.Utils/IO/IcdPath.cs +++ b/ICD.Common.Utils/IO/IcdPath.cs @@ -10,6 +10,10 @@ namespace ICD.Common.Utils.IO { public static class IcdPath { + public static char DirectorySeparatorChar { get { return Path.DirectorySeparatorChar; } } + + public static char AltDirectorySeparatorChar { get { return Path.AltDirectorySeparatorChar; } } + public static string GetFileName(string path) { if (path == null) diff --git a/ICD.Common.Utils/Json/AbstractGenericJsonConverter.cs b/ICD.Common.Utils/Json/AbstractGenericJsonConverter.cs index 151a7ca..ced8956 100644 --- a/ICD.Common.Utils/Json/AbstractGenericJsonConverter.cs +++ b/ICD.Common.Utils/Json/AbstractGenericJsonConverter.cs @@ -6,6 +6,15 @@ namespace ICD.Common.Utils.Json { public abstract class AbstractGenericJsonConverter : JsonConverter { + /// + /// Creates a new instance of T. + /// + /// + protected virtual T Instantiate() + { + return ReflectionUtils.CreateInstance(); + } + /// /// Writes the JSON representation of the object. /// @@ -36,7 +45,24 @@ namespace ICD.Common.Utils.Json /// The value. /// The calling serializer. [PublicAPI] - public abstract void WriteJson(JsonWriter writer, T value, JsonSerializer serializer); + public virtual void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) + { + writer.WriteStartObject(); + { + WriteProperties(writer, value, serializer); + } + writer.WriteEndObject(); + } + + /// + /// Override to write properties to the writer. + /// + /// + /// + /// + protected virtual void WriteProperties(JsonWriter writer, T value, JsonSerializer serializer) + { + } /// /// Reads the JSON representation of the object. @@ -70,7 +96,52 @@ namespace ICD.Common.Utils.Json /// The object value. /// [PublicAPI] - public abstract T ReadJson(JsonReader reader, T existingValue, JsonSerializer serializer); + public virtual T ReadJson(JsonReader reader, T existingValue, JsonSerializer serializer) + { + T output = default(T); + bool instantiated = false; + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.Null || reader.TokenType == JsonToken.EndObject) + break; + + if (!instantiated) + { + instantiated = true; + output = Instantiate(); + } + + // Get the property + if (reader.TokenType != JsonToken.PropertyName) + continue; + string property = (string)reader.Value; + + // Read into the value + reader.Read(); + + switch (property) + { + default: + ReadProperty(property, reader, output, serializer); + break; + } + } + + return output; + } + + /// + /// Override to handle the current property value with the given name. + /// + /// + /// + /// + /// + protected virtual void ReadProperty(string property, JsonReader reader, T instance, JsonSerializer serializer) + { + reader.Skip(); + } /// /// Determines whether this instance can convert the specified object type. diff --git a/ICD.Common.Utils/Json/JsonUtils.cs b/ICD.Common.Utils/Json/JsonUtils.cs index 8bb7a9c..6449cd7 100644 --- a/ICD.Common.Utils/Json/JsonUtils.cs +++ b/ICD.Common.Utils/Json/JsonUtils.cs @@ -107,9 +107,21 @@ namespace ICD.Common.Utils.Json /// [PublicAPI] public static void Print(object value) + { + string formatted = Format(value); + IcdConsole.PrintLine(formatted); + } + + /// + /// Serializes the given item and formats the JSON into a human-readable form. + /// + /// + /// + [PublicAPI] + public static string Format(object value) { string serial = JsonConvert.SerializeObject(value); - Print(serial); + return Format(serial); } /// diff --git a/ICD.Common.Utils/PathUtils.cs b/ICD.Common.Utils/PathUtils.cs index 7a691e1..3d473a6 100644 --- a/ICD.Common.Utils/PathUtils.cs +++ b/ICD.Common.Utils/PathUtils.cs @@ -185,6 +185,24 @@ namespace ICD.Common.Utils return IcdFile.Exists(path) || IcdDirectory.Exists(path); } + /// + /// Returns the path if the given path is already a directory or has a trailing slash. + /// Otherwise returns the parent directory name. + /// + /// + /// + [PublicAPI] + public static string GetDirectoryNameFromPath(string path) + { + if (IcdDirectory.Exists(path)) + return path; + + if (path.EndsWith(IcdPath.DirectorySeparatorChar) || path.EndsWith(IcdPath.AltDirectorySeparatorChar)) + return path; + + return IcdPath.GetDirectoryName(path); + } + #endregion } } diff --git a/ICD.Common.Utils/Properties/AssemblyInfo.cs b/ICD.Common.Utils/Properties/AssemblyInfo.cs index 4a435e7..2303f51 100644 --- a/ICD.Common.Utils/Properties/AssemblyInfo.cs +++ b/ICD.Common.Utils/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ using System.Reflection; [assembly: AssemblyCompany("ICD Systems")] [assembly: AssemblyProduct("ICD.Common.Utils")] [assembly: AssemblyCopyright("Copyright © ICD Systems 2018")] -[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyVersion("3.0.0.0")] diff --git a/ICD.Common.Utils/ReflectionUtils.cs b/ICD.Common.Utils/ReflectionUtils.cs index 94a9542..534ca34 100644 --- a/ICD.Common.Utils/ReflectionUtils.cs +++ b/ICD.Common.Utils/ReflectionUtils.cs @@ -195,6 +195,24 @@ namespace ICD.Common.Utils != null; } + /// + /// Platform independant delegate instantiation. + /// + /// + /// + /// + /// + public static Delegate CreateDelegate(Type type, object firstArgument, MethodInfo method) + { + return +#if SIMPLSHARP + CDelegate +#else + Delegate +#endif + .CreateDelegate(type, firstArgument, method); + } + /// /// Creates an instance of the given type, calling the default constructor. /// @@ -368,24 +386,71 @@ namespace ICD.Common.Utils if (type.CanBeNull()) return null; - throw new InvalidCastException(); + throw new InvalidCastException(string.Format("Unable to convert NULL to type {0}", type.Name)); } Type valueType = value.GetType(); if (valueType.IsAssignableTo(type)) return value; - // Handle enum - if (type.IsEnum) + try { - if (valueType.IsIntegerNumeric()) - return Enum.ToObject(type, value); + // Handle enum + if (type.IsEnum) + { + if (valueType.IsIntegerNumeric()) + return Enum.ToObject(type, value); - if (value is string) - return Enum.Parse(type, value as string, false); + if (value is string) + return Enum.Parse(type, value as string, false); + } + + return Convert.ChangeType(value, type, null); } + catch (Exception e) + { + string valueString = valueType.ToString(); + string message = string.Format("Failed to convert {0} to type {1} - {2}", valueString, type, e.Message); + throw new InvalidCastException(message, e); + } + } - return Convert.ChangeType(value, type, null); + /// + /// Subscribes to the event on the given instance using the handler and callback method. + /// + /// The instance with the event. Null for static types. + /// The EventInfo for the event. + /// The instance with the callback MethodInfo. Null for static types. + /// The MethodInfo for the callback method. + /// + public static Delegate SubscribeEvent(object instance, EventInfo eventInfo, object handler, MethodInfo callback) + { + if (eventInfo == null) + throw new ArgumentNullException("eventInfo"); + + if (callback == null) + throw new ArgumentNullException("callback"); + + Delegate output = CreateDelegate(eventInfo.EventHandlerType, handler, callback); + eventInfo.AddEventHandler(instance, output); + return output; + } + + /// + /// Unsubscribes from the event on the given instance. + /// + /// The instance with the event. Null for static types. + /// The EventInfo for the event. + /// The Delegate to be removed from the event. + public static void UnsubscribeEvent(object instance, EventInfo eventInfo, Delegate callback) + { + if (eventInfo == null) + throw new ArgumentNullException("eventInfo"); + + if (callback == null) + throw new ArgumentNullException("callback"); + + eventInfo.RemoveEventHandler(instance, callback); } } } diff --git a/ICD.Common.Utils/ReprBuilder.cs b/ICD.Common.Utils/ReprBuilder.cs index 6204d52..481feae 100644 --- a/ICD.Common.Utils/ReprBuilder.cs +++ b/ICD.Common.Utils/ReprBuilder.cs @@ -3,6 +3,8 @@ using System.Text; namespace ICD.Common.Utils { + public delegate void AddReprPropertyDelegate(string name, object value); + /// /// Simple class for building a string representation of an object. /// diff --git a/ICD.Common.Utils/StringUtils.cs b/ICD.Common.Utils/StringUtils.cs index 36008e7..3235b7b 100644 --- a/ICD.Common.Utils/StringUtils.cs +++ b/ICD.Common.Utils/StringUtils.cs @@ -610,5 +610,77 @@ namespace ICD.Common.Utils { return value == null ? null : value.ToUpper(); } + + /// + /// Compares the given chars for equality. + /// + /// + /// + /// + /// + [PublicAPI] + public static bool Compare(char a, char b, bool ignoreCase) + { + if (ignoreCase) + { + a = char.ToUpper(a, CultureInfo.InvariantCulture); + b = char.ToUpper(b, CultureInfo.InvariantCulture); + } + + return a == b; + } + + /// + /// Find the longest common string between the matches. + /// E.g. + /// + /// C:\\Workspace + /// C:\\Workshop + /// + /// Results in + /// + /// C:\\Work + /// + /// + /// + /// + [PublicAPI] + public static string GetLongestCommonIntersectionFromStart(IEnumerable items, bool ignoreCase) + { + if (items == null) + throw new ArgumentNullException("items"); + + string output = null; + + foreach (string item in items) + { + // If there is a null in the sequence that's the best match we can make + if (string.IsNullOrEmpty(item)) + return null; + + // Seed our first item + if (output == null) + { + output = item; + continue; + } + + // Find the common substring + for (int index = 0; index < output.Length; index++) + { + if (index >= item.Length || !Compare(output[index], item[index], ignoreCase)) + { + output = output.Substring(0, index); + break; + } + } + + // Abandon the search if there is no common substring + if (string.IsNullOrEmpty(output)) + break; + } + + return output; + } } } diff --git a/ICD.Common.Utils/Timers/IcdStopwatch.cs b/ICD.Common.Utils/Timers/IcdStopwatch.cs index 1f9f398..c930671 100644 --- a/ICD.Common.Utils/Timers/IcdStopwatch.cs +++ b/ICD.Common.Utils/Timers/IcdStopwatch.cs @@ -140,6 +140,7 @@ namespace ICD.Common.Utils.Timers { using (IEnumerator enumerator = enumerable.GetEnumerator()) { +// ReSharper disable once AccessToDisposedClosure while (Profile(() => enumerator.MoveNext(), name)) yield return enumerator.Current; }