From 773e998198d0d4213b226c79ce6c3a73f17c151a Mon Sep 17 00:00:00 2001 From: grandsilence Date: Thu, 19 Jul 2018 23:30:12 +0300 Subject: [PATCH] New String extensions: EscapeJsonData, ToFileFormatString. New Exception extensions: GetDetailedMessage (with call stack trice and innerExceptions). ProcessAgregated for Processing InnerExceptions when AggregatedException thrown. New NumberExtensions: PercentageString (for float, decemial, double) LockedCollections returned back. --- .../Generic/CollectionRandomizer.cs | 35 ++++ Leaf.Core/Collections/Generic/IStorage.cs | 31 +++ Leaf.Core/Collections/Generic/ListStorage.cs | 50 +++++ .../Collections/Generic/LockedCollection.cs | 70 +++++++ .../Collections/Generic/LockedFactory.cs | 195 ++++++++++++++++++ Leaf.Core/Collections/Generic/LockedList.cs | 143 +++++++++++++ Leaf.Core/Collections/Generic/LockedQueue.cs | 32 +++ Leaf.Core/Collections/Generic/QueueStorage.cs | 17 ++ Leaf.Core/Collections/LockedList.cs | 37 ++++ Leaf.Core/Collections/LockedQueue.cs | 43 ++++ .../Extensions/String/StringExtensions.cs | 15 +- .../Extensions/System/DateTimeExtensions.cs | 10 +- .../Extensions/System/ExceptionExtensions.cs | 91 ++++++++ .../System/NumberFormatExtensions.cs | 71 +++++++ .../Extensions/System/TimeSpanExtensions.cs | 1 - 15 files changed, 838 insertions(+), 3 deletions(-) create mode 100644 Leaf.Core/Collections/Generic/CollectionRandomizer.cs create mode 100644 Leaf.Core/Collections/Generic/IStorage.cs create mode 100644 Leaf.Core/Collections/Generic/ListStorage.cs create mode 100644 Leaf.Core/Collections/Generic/LockedCollection.cs create mode 100644 Leaf.Core/Collections/Generic/LockedFactory.cs create mode 100644 Leaf.Core/Collections/Generic/LockedList.cs create mode 100644 Leaf.Core/Collections/Generic/LockedQueue.cs create mode 100644 Leaf.Core/Collections/Generic/QueueStorage.cs create mode 100644 Leaf.Core/Collections/LockedList.cs create mode 100644 Leaf.Core/Collections/LockedQueue.cs create mode 100644 Leaf.Core/Extensions/System/ExceptionExtensions.cs create mode 100644 Leaf.Core/Extensions/System/NumberFormatExtensions.cs diff --git a/Leaf.Core/Collections/Generic/CollectionRandomizer.cs b/Leaf.Core/Collections/Generic/CollectionRandomizer.cs new file mode 100644 index 0000000..0c0c518 --- /dev/null +++ b/Leaf.Core/Collections/Generic/CollectionRandomizer.cs @@ -0,0 +1,35 @@ +using System; + +namespace Leaf.Core.Collections.Generic +{ + /// + /// Расширения для потокобезопасных коллекций, реализующие работу со случайностями. + /// + public static class CollectionRandimizer + { + [ThreadStatic] private static Random _rand; + private static Random Rand => _rand ?? (_rand = new Random()); + + /// + /// Получает случайный элемент коллекции + /// + /// + /// + /// + /// + /// HACK: Решение для Genetic Random. + public static T GetNextRandom(this ListStorage collection) + { + if (collection.Count == 0) + return default(T); + + int index = Rand.Next(collection.Count - 1); + var result = collection[index]; + + if (collection.Iteration == ListIteration.Removable) + collection.RemoveAt(index); + + return result; + } + } +} diff --git a/Leaf.Core/Collections/Generic/IStorage.cs b/Leaf.Core/Collections/Generic/IStorage.cs new file mode 100644 index 0000000..45cabb5 --- /dev/null +++ b/Leaf.Core/Collections/Generic/IStorage.cs @@ -0,0 +1,31 @@ +namespace Leaf.Core.Collections.Generic +{ + /// + /// Реализация потокобезопасного хранилища. + /// + /// Тип хранимых объектов + public interface IStorage + { + /// + /// Число элементов в коллекции. + /// + int Count { get; } + + /// + /// Очистить коллекцию. + /// + void Clear(); + + /// + /// Добавляет элемент в коллекцию. + /// + /// + void AppendItem(T item); + + /// + /// Возвращает следующий элемент коллекции. + /// + /// Следующий элемент коллекции. + T GetNext(); + } +} diff --git a/Leaf.Core/Collections/Generic/ListStorage.cs b/Leaf.Core/Collections/Generic/ListStorage.cs new file mode 100644 index 0000000..109928f --- /dev/null +++ b/Leaf.Core/Collections/Generic/ListStorage.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace Leaf.Core.Collections.Generic +{ + public sealed class ListStorage : List, IStorage + { + public ListIteration Iteration = ListIteration.TillTheEnd; + private int _currentIndex; + + /// + /// Сбрасывает текущий элемент списка на первый (нулевой). + /// + public void ResetPointer() + { + _currentIndex = 0; + } + + void IStorage.AppendItem(T item) + { + Add(item); + } + + T IStorage.GetNext() + { + if (Count == 0) + return default(T); + + if (_currentIndex >= Count) + { + if (Iteration == ListIteration.TillTheEnd) + return default(T); + + _currentIndex = 0; + } + + var result = this[_currentIndex]; + if (Iteration == ListIteration.Removable) + { + RemoveAt(_currentIndex); + // Don't increment index! + } + else + { + ++_currentIndex; + } + + return result; + } + } +} diff --git a/Leaf.Core/Collections/Generic/LockedCollection.cs b/Leaf.Core/Collections/Generic/LockedCollection.cs new file mode 100644 index 0000000..257e1b9 --- /dev/null +++ b/Leaf.Core/Collections/Generic/LockedCollection.cs @@ -0,0 +1,70 @@ +namespace Leaf.Core.Collections.Generic +{ + /// + /// Абстрактная реализация потокобезопасной коллекции. + /// + /// Тип хранимых объектов + public abstract class LockedCollection // TODO : IEnumerable + { + /// + /// Коллекция реализующая общие методы для работы с элементами. + /// + protected IStorage Storage; + + /// + /// Удаляет все элементы из коллекции. + /// + public virtual void Clear() + { + lock (Storage) + Storage.Clear(); + } + + /// + /// Доступное число элементов в коллекции. + /// + public virtual int Count + { + get { + lock (Storage) + return Storage.Count; + } + } + + /// + /// Возвращает следующий элемент коллекции. Если коллекция пуста будет возвращён null. + /// + public virtual T GetNext() + { + if (Storage == null) + return default(T); + + lock (Storage) + return Storage.GetNext(); + } + + /// + /// Записывает следующий элемент коллекции в переменную. + /// + /// Переменная куда будет записан полученный элемент из коллекции + /// Вернет истину если элемент был возращен. Если коллеция уже пуста вернет ложь. + public virtual bool GetNext(out T item) + { + item = GetNext(); + return item != null; + } + + /// + /// Добавляет элемент в коллекцию. + /// + /// Элемент + public virtual void AppendItem(T item) + { + if (Storage == null) + return; + + lock (Storage) + Storage.AppendItem(item); + } + } +} diff --git a/Leaf.Core/Collections/Generic/LockedFactory.cs b/Leaf.Core/Collections/Generic/LockedFactory.cs new file mode 100644 index 0000000..2695d4f --- /dev/null +++ b/Leaf.Core/Collections/Generic/LockedFactory.cs @@ -0,0 +1,195 @@ +using System.IO; +using System.Threading.Tasks; +using Leaf.Core.Runtime.Serialization; +using Leaf.Core.Threading; + +namespace Leaf.Core.Collections.Generic +{ + /// + /// Фабрика потокобезопасных коллекций. + /// + public static class LockedFactory + { + #region # Public Synchronous Methods + + #region ## Generic + + /// + /// Создаёт потокобезопасный список объектов из текстового файла. Десериализация проводится построчно. + /// + /// Путь до файла + /// Потокобезопасный интерфейс, нужен для ведения лога в случае ошибки десериализации + /// Если true, то строки с коментариями тоже будут включены в выборку для десериалиализации. + /// Очищать начало и конец строк от отступов и пробелов. + /// Возвращает новый потокобезопасный список объектов + public static LockedList ListFromFile(string filePath, ThreadSafeUI ui = null, + bool includeComments = false, bool trim = true) + where T : IStringSerializeable, new() + { + var result = new LockedList(); + ReadAndDeserialize(result, filePath, ui, includeComments, trim); + return result; + } + + /// + /// Создаёт потокобезопасную очередь объектов из текстового файла. Десериализация проводится построчно. + /// + /// Путь до файла + /// Потокобезопасный интерфейс, нужен для ведения лога в случае ошибки десериализации + /// Если true, то строки с коментариями тоже будут включены в выборку для десериалиализации. + /// Очищать начало и конец строк от отступов и пробелов. + /// Возвращает новую потокобезопасную очередь объектов + public static LockedQueue QueueFromFile(string filePath, ThreadSafeUI ui = null, + bool includeComments = false, bool trim = true) + where T : IStringSerializeable, new() + { + var result = new LockedQueue(); + ReadAndDeserialize(result, filePath, ui, includeComments, trim); + return result; + } + + #endregion + + #region ## String + + /// + /// + /// Создаёт потокобезопасный список строк из текстового файла. Десериализация проводится построчно. + /// + /// Возвращает новый потокобезопасный список строк + public static LockedList ListFromFile(string filePath, bool includeComments = false, bool trim = true) + { + var result = new LockedList(); + ReadAndAppend(result, filePath, includeComments, trim); + return result; + } + + /// + /// + /// Создаёт потокобезопасную очередь строк из текстового файла. Десериализация проводится построчно. + /// + /// Возвращает новую потокобезопасную очередь строк + public static LockedQueue QueueFromFile(string filePath, bool includeComments = false, bool trim = true) + { + var result = new LockedQueue(); + ReadAndAppend(result, filePath, includeComments, trim); + return result; + } + + #endregion + + #endregion + + #region # Public Asynchronous Methods + + #region ## Generic + + /// + /// + /// Асинхронно создаёт потокобезопасный список объектов из текстового файла. Десериализация проводится построчно. + /// + public static async Task> ListFromFileAsync(string filePath, ThreadSafeUI ui = null, + bool includeComments = false, bool trim = true) + where T : IStringSerializeable, new() + { + return await Task.Run(() => ListFromFile(filePath, ui, includeComments, trim)); + } + + /// + /// + /// Асинхронно создаёт потокобезопасную очередь объектов из текстового файла. Десериализация проводится построчно. + /// + public static async Task> QueueFromFileAsync(string filePath, ThreadSafeUI ui = null, + bool includeComments = false, bool trim = true) + where T : IStringSerializeable, new() + { + return await Task.Run(() => QueueFromFile(filePath, ui, includeComments, trim)); + } + + #endregion + + #region ## String + + /// + /// + /// Асинхронно создаёт потокобезопасный список строк из текстового файла. Десериализация проводится построчно. + /// + public static async Task ListFromFileAsync(string filePath, + bool includeComments = false, bool trim = true) + { + return await Task.Run(() => ListFromFile(filePath, includeComments, trim)); + } + + /// + /// + /// Асинхронно создаёт потокобезопасную очередь строк из текстового файла. Десериализация проводится построчно. + /// + public static async Task QueueFromFileAsync(string filePath, + bool includeComments = false, bool trim = true) + { + return await Task.Run(() => QueueFromFile(filePath, includeComments, trim)); + } + + #endregion + + #endregion + + #region Private helpers + private delegate void LineProcessor(ulong lineNumber, string line); + + private static void ReadFileLineByLine(string filePath, bool includeComments, bool trim, LineProcessor lineProcessor) + { + if (!File.Exists(filePath)) + { + File.Create(filePath).Close(); + return; + } + + using (var file = new StreamReader(filePath)) + { + ulong lineNumber = 0; + + while (!file.EndOfStream) + { + string line = file.ReadLine(); + ++lineNumber; + + // Пропускаем пустые строки и комментарии если требуется + if (string.IsNullOrWhiteSpace(line) || + !includeComments && (line.StartsWith("//") || line.StartsWith("#"))) + continue; + + if (trim) + line = line.Trim(); + + lineProcessor(lineNumber, line); + } + } + } + + private static void ReadAndDeserialize(LockedCollection collection, string filePath, ThreadSafeUI ui, + bool includeComments, bool trim) + where T : IStringSerializeable, new() + { + ReadFileLineByLine(filePath, includeComments, trim, (lineNumber, line) => { + // Десериализуем объект из строки + var item = new T(); + if (item.DeserializeFromString(line)) + { + collection.AppendItem(item); + } + else + ui?.Log($"Skipped {typeof(T).Name} because invalid format. Line #{lineNumber}: {line}"); + }); + } + + private static void ReadAndAppend(LockedCollection collection, string filePath, + bool includeComments, bool trim) + { + ReadFileLineByLine(filePath, includeComments, trim, (lineNumber, line) => { + collection.AppendItem(line); + }); + } + #endregion + } +} diff --git a/Leaf.Core/Collections/Generic/LockedList.cs b/Leaf.Core/Collections/Generic/LockedList.cs new file mode 100644 index 0000000..91cbb17 --- /dev/null +++ b/Leaf.Core/Collections/Generic/LockedList.cs @@ -0,0 +1,143 @@ +using System; +//using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Leaf.Core.Collections.Generic +{ + // TODO: use object lockers. Different for read and write. + // TODO: concurrent alternatives http://www.c-sharpcorner.com/article/thread-safe-concurrent-collection-in-C-Sharp/ + + /// + /// + /// Потокобезопасный список. + /// + public class LockedList : LockedCollection + { + protected readonly ListStorage ListStorage = new ListStorage(); + + /// + /// Изначальное число элементов. Назначение: подсчет прогресса. + /// Значение задается на фабриках, например после чтения коллекции из файла. + /// + public int StartCount { get; set; } + + /// + /// Создает новый потокобезопасный список. + /// + public LockedList() + { + Storage = ListStorage; + } + + /// + /// + /// Создает новый потокобезопасный список на основе перечислимой коллекции. + /// + /// Элементы которые следует добавить в список + public LockedList(IEnumerable items) : this() // Вызываем базовый конструктор сначала + { + foreach (var item in items) + ListStorage.Add(item); + } + + /// + /// Возвращает следующий случайный элемент из списка. + /// + public T GetNextRandom() + { + if (Storage == null) + return default(T); + + lock (Storage) + return ListStorage.GetNextRandom(); + } + + /// + /// Устанавливает тип перечисления элементов списка. + /// + public ListIteration Iteration { + get { + lock (Storage) + return ListStorage.Iteration; + } + set { + lock (Storage) + ListStorage.Iteration = value; + } + } + + /// + /// Сбрасывает указатель текущего элемента списка на первый (нулевой индекс). + /// + public void ResetPointer() + { + lock (ListStorage) + { + ListStorage.ResetPointer(); + } + } + + /// + /// Проверяет существование элемента в списке. + /// + public bool Contains(T item) + { + if (Storage == null) + return false; + + lock (Storage) + { + return ListStorage.Contains(item); + } + } + + /// + /// Удаляет элемент из списка. + /// + /// Элемент + /// Возвращает истину, если элемент был найден и удалён. + public bool Remove(T item) + { + if (Storage == null) + return false; + + lock (Storage) + { + return ListStorage.Remove(item); + } + } + + public IEnumerable Where(Func predicate) + { + lock (Storage) + { + return ListStorage.Where(predicate); + } + } + + public IEnumerable Where(Func predicate) + { + lock (Storage) + { + return ListStorage.Where(predicate); + } + } + + public T First(Func predicate) + { + lock (Storage) + { + return ListStorage.First(predicate); + } + } + + public ListStorage GetUnsafeStorage() + { + lock (ListStorage) + { + return ListStorage; + } + } + } +} diff --git a/Leaf.Core/Collections/Generic/LockedQueue.cs b/Leaf.Core/Collections/Generic/LockedQueue.cs new file mode 100644 index 0000000..2349ec4 --- /dev/null +++ b/Leaf.Core/Collections/Generic/LockedQueue.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Leaf.Core.Collections.Generic +{ + /// + /// + /// Потокобезопасная очередь. + /// + public class LockedQueue : LockedCollection + { + protected readonly QueueStorage QueueStorage = new QueueStorage(); + + /// + /// Создает новую потокобезопасную очередь. + /// + public LockedQueue() + { + Storage = QueueStorage; + } + + /// + /// + /// Создает новую потокобезопасную очередь на основе перечислимой коллекции. + /// + /// Элементы которые следует добавить в очередь + public LockedQueue(IEnumerable items) : this() // Вызываем базовый конструктор сначала + { + foreach (var item in items) + QueueStorage.Enqueue(item); + } + } +} diff --git a/Leaf.Core/Collections/Generic/QueueStorage.cs b/Leaf.Core/Collections/Generic/QueueStorage.cs new file mode 100644 index 0000000..25dbecc --- /dev/null +++ b/Leaf.Core/Collections/Generic/QueueStorage.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Leaf.Core.Collections.Generic +{ + public class QueueStorage : Queue, IStorage + { + public void AppendItem(T item) + { + Enqueue(item); + } + + public T GetNext() + { + return Count == 0 ? default(T) : Dequeue(); + } + } +} diff --git a/Leaf.Core/Collections/LockedList.cs b/Leaf.Core/Collections/LockedList.cs new file mode 100644 index 0000000..66ef6cc --- /dev/null +++ b/Leaf.Core/Collections/LockedList.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Leaf.Core.Collections.Generic; + +namespace Leaf.Core.Collections +{ + /// + /// Определяет тип перечисления списка. + /// + public enum ListIteration + { + /// + /// Список будет перечисляться пока не достигнет конца. Элементы не удаляются. + /// + TillTheEnd, + /// + /// Список будет перечисляться бесконечно - после последнего элемента следует первый. + /// + Looped, + /// + /// Список будет перечисляться пока не достигнет конца. Перечисленные элементы удаляются. + /// + Removable + } + + /// + /// + /// Потокобезопасный список строк. + /// + public class LockedList : LockedList + { + public LockedList() { } + + public LockedList(IEnumerable items) : base(items) + { + } + } +} diff --git a/Leaf.Core/Collections/LockedQueue.cs b/Leaf.Core/Collections/LockedQueue.cs new file mode 100644 index 0000000..b770a90 --- /dev/null +++ b/Leaf.Core/Collections/LockedQueue.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text; +using Leaf.Core.Collections.Generic; + +namespace Leaf.Core.Collections +{ + /// + /// + /// Потокобезопасная очередь строк. + /// + public class LockedQueue : LockedQueue + { + public LockedQueue() {} + + public LockedQueue(IEnumerable items) : base(items) {} + + /// + /// Поочередно извлекает все элементы из очереди и записывает каждый в общую строку. + /// + /// Строка со всеми элементами. Каждый элементами будет идти с новой строки. + public string DeqeueAllToString() + { + if (Storage == null) + return string.Empty; + + var sb = new StringBuilder(); + + lock (Storage) + { + int materialsCount = Storage.Count; + + for (int i = 0; i < materialsCount; i++) + { + string material = Storage.GetNext(); + if (!string.IsNullOrEmpty(material)) + sb.AppendLine(material.Trim()); + } + } + + return sb.ToString(); + } + } +} diff --git a/Leaf.Core/Extensions/String/StringExtensions.cs b/Leaf.Core/Extensions/String/StringExtensions.cs index cf5c3b7..e1a03d2 100644 --- a/Leaf.Core/Extensions/String/StringExtensions.cs +++ b/Leaf.Core/Extensions/String/StringExtensions.cs @@ -11,7 +11,7 @@ namespace Leaf.Core.Extensions.String public static class StringExtensions { /// - /// Проверяет наличие слова в строке, аналогично , но без учета реестра и региональных стандартов. + /// Проверяет наличие слова в строке, аналогично , но без учета реестра и региональных стандартов. /// /// Строка для поиска слова /// Слово которое должно содержаться в строке @@ -103,6 +103,19 @@ public static byte[] HexStringToBytes(this string hexString) return bytes; } + /// + /// Экранирует " символы и символы юникода в JSON. + /// + /// JSON данные + /// Вернет экранированные данные. + public static string EscapeJsonData(this string jsonData) + { + return jsonData + .Replace("\"", "\\\"") + .Replace("\\", "\\\\") + .EncodeJsonUnicode(); + } + /// /// Возращает тип форматирование числа с разделением тысяч. /// diff --git a/Leaf.Core/Extensions/System/DateTimeExtensions.cs b/Leaf.Core/Extensions/System/DateTimeExtensions.cs index 7745b85..6d8572f 100644 --- a/Leaf.Core/Extensions/System/DateTimeExtensions.cs +++ b/Leaf.Core/Extensions/System/DateTimeExtensions.cs @@ -1,5 +1,5 @@ using System; -// ReSharper disable UnusedMember.Global +using System.Globalization; namespace Leaf.Core.Extensions.System { @@ -17,5 +17,13 @@ public static DateTime FirstJanuary1970 private static DateTime _firstJanuary1970; public static ulong MillisecondsFrom1970 => (ulong) (DateTime.UtcNow - FirstJanuary1970).TotalMilliseconds; + + /// + /// Время в безопасном формате для наименования файла. + /// + public static string ToFileFormatString(this DateTime self) + { + return self.ToString("yyyy-MM-dd__HH:mm:ss", CultureInfo.InvariantCulture); + } } } \ No newline at end of file diff --git a/Leaf.Core/Extensions/System/ExceptionExtensions.cs b/Leaf.Core/Extensions/System/ExceptionExtensions.cs new file mode 100644 index 0000000..e71f229 --- /dev/null +++ b/Leaf.Core/Extensions/System/ExceptionExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Text; + +namespace Leaf.Core.Extensions.System +{ + /// + /// Делегат, обрабатывающий исключение внутри нескольких исключений (объединённых). + /// + /// Исключение + /// вернет истину если ошибка была обработана и не следует бросать исключение выше. + public delegate bool DProcessAgregated(Exception ex); + + // ReSharper disable once UnusedMember.Global + public static class ExceptionExtensions + { + public static int MaxExceptionInnerLevelDetails + { + get => _maxExceptionInnerLevelDetails; + set { + if (value <= 0) + throw new ArgumentException("Invalid value. It must be greater than 0", nameof(MaxExceptionInnerLevelDetails)); + + _maxExceptionInnerLevelDetails = value; + } + } + private static int _maxExceptionInnerLevelDetails = 3; + + /// + /// Возвращает детальное сообщение исключения с включением внутренней ошибки, если она присутствует. + /// + /// Исключение + /// Включить ли стек вызовов в сообщение + /// Полное сообщение об исключении + public static string GetDetailedMessage(this Exception ex, bool stackTrace = true) + { + //if (maxInnerLevel <= 0) + //maxInnerLevel = MaxExceptionInnerLevelDetails; + //throw new ArgumentException("Invalid value. It must be greater than 0", nameof(maxInnerLevel)); + + var sb = new StringBuilder(); + int innerLevel = 1; + + sb.AppendFormat("Exception: \"{0}\"", ex.GetType().Name); + if (ex.Message != null) { + sb.AppendFormat(". Message: {0}", ex.Message); + } + if (stackTrace) + { + sb.AppendLine(); + sb.Append(ex.StackTrace); + } + + var innerException = ex.InnerException; + + while (innerException != null && innerLevel <= MaxExceptionInnerLevelDetails) + { + string innerType = innerException.GetType().ToString(); + sb.AppendLine(); + sb.AppendFormat("EX #{0} InnerException: \"{1}\"", innerLevel, innerType); + if (innerException.Message != null) { + sb.AppendFormat(". Message: {0}", innerException.Message); + } + + innerException = innerException.InnerException; + ++innerLevel; + } + return sb.ToString(); + } + + /// + /// Обрабатывает несколько исключений (агрегация) через делегат-обработчик. + /// + /// Возможно агрегированное исключение + /// Обработчик + /// вернет истину если ошибка была обработана и не следует бросать исключение выше. + public static bool ProcessAgregated(this Exception ex, DProcessAgregated handler) + { + if (!(ex is AggregateException ag)) + return handler(ex); + + // Process all inner exceptions - if any false - return false + foreach (var innerException in ag.Flatten().InnerExceptions) + { + if (!handler(innerException)) + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Leaf.Core/Extensions/System/NumberFormatExtensions.cs b/Leaf.Core/Extensions/System/NumberFormatExtensions.cs new file mode 100644 index 0000000..f35155d --- /dev/null +++ b/Leaf.Core/Extensions/System/NumberFormatExtensions.cs @@ -0,0 +1,71 @@ +using System.Globalization; +using Leaf.Core.Extensions.String; + +namespace Leaf.Core.Extensions.System +{ + /// + /// Расширения для форматирования чисел к виду с разделителем тысяч. + /// + public static class NumberFormatExtensions + { + /// + /// Вернет число в виде строки в которой разряды тысяч разделяются пробелом. + /// + public static string PrettyString(this ulong self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + public static string PrettyString(this long self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + public static string PrettyString(this uint self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + public static string PrettyString(this int self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + public static string PrettyString(this ushort self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + public static string PrettyString(this short self) + { + return self.ToString(StringExtensions.ThousandNumberFormatInfo); + } + + /// + /// Вернет число в виде строки "###.## %". + /// + public static string PercentageString(this float self) + { + return self.ToString("###.##", CultureInfo.InvariantCulture) + SpaceAndPercent; + } + + /// + public static string PercentageString(this double self) + { + return self.ToString("###.##", CultureInfo.InvariantCulture) + SpaceAndPercent; + } + + /// + public static string PercentageString(this decimal self) + { + return self.ToString("###.##", CultureInfo.InvariantCulture) + SpaceAndPercent; + } + + private const string SpaceAndPercent = " %"; + } +} diff --git a/Leaf.Core/Extensions/System/TimeSpanExtensions.cs b/Leaf.Core/Extensions/System/TimeSpanExtensions.cs index b7b3fca..768ccf0 100644 --- a/Leaf.Core/Extensions/System/TimeSpanExtensions.cs +++ b/Leaf.Core/Extensions/System/TimeSpanExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Text; -// ReSharper disable UnusedMember.Global namespace Leaf.Core.Extensions.System {