diff --git a/DNX.Helpers/DNX.Helpers.csproj b/DNX.Helpers/DNX.Helpers.csproj index 8160ec6..fac36a8 100644 --- a/DNX.Helpers/DNX.Helpers.csproj +++ b/DNX.Helpers/DNX.Helpers.csproj @@ -1,6 +1,7 @@  netstandard2.0 + latest true .NET Extensions and helpers for Core and Common .NET types Martin Smith @@ -12,9 +13,9 @@ https://raw.githubusercontent.com/martinsmith1968/DNX.Helpers/master/images/favicon-32x32.png DNX helpers extensions string array linq Interpolation to a working version and some preparation for moving to .NET Standard - 2.0.8 - 2.0.8 - 2.0.8 + 2.1.0 + 2.1.0 + 2.1.0 bin\DNX.Helpers.xml @@ -70,4 +71,4 @@ GuardBuiltInTypesTemplate.tt - \ No newline at end of file + diff --git a/DNX.Helpers/Internal/RunSafely.cs b/DNX.Helpers/Internal/RunSafely.cs new file mode 100644 index 0000000..193984e --- /dev/null +++ b/DNX.Helpers/Internal/RunSafely.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; + +namespace DNX.Helpers.Internal +{ + /// + /// RunSafely - execute code with defaulted exception handling + /// + public class RunSafely + { + /// + /// Executes the specified action. + /// + /// The action. + /// The exception handler. + public static void Execute(Action action, Action exceptionHandler = null) + { + try + { + action.Invoke(); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + } + } + + /// + /// Executes the specified function. + /// + /// + /// The function. + /// The exception handler. + /// T. + public static T Execute(Func func, Action exceptionHandler = null) + { + return Execute(func, default, exceptionHandler); + } + + /// + /// Executes the specified function. + /// + /// + /// The function. + /// The default value. + /// The exception handler. + /// T. + public static T Execute(Func func, T defaultValue, Action exceptionHandler = null) + { + try + { + return func.Invoke(); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + + return defaultValue; + } + } + + /// + /// execute as an asynchronous operation. + /// + /// The task. + /// The exception handler. + public static async Task ExecuteAsync(Task task, Action exceptionHandler = null) + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + } + } + + /// + /// execute as an asynchronous operation. + /// + /// + /// The task. + /// The exception handler. + /// Task<T>. + public static async Task ExecuteAsync(Task task, Action exceptionHandler = null) + { + return await ExecuteAsync(task, default, exceptionHandler); + } + + /// + /// execute as an asynchronous operation. + /// + /// + /// The task. + /// The default value. + /// The exception handler. + /// Task<T>. + public static async Task ExecuteAsync(Task task, T defaultValue, Action exceptionHandler = null) + { + try + { + return await task.ConfigureAwait(false); + } + catch (Exception ex) + { + Execute(() => exceptionHandler?.Invoke(ex)); + + return defaultValue; + } + } + } +} diff --git a/DNX.Helpers/Strings/StringExtensions.cs b/DNX.Helpers/Strings/StringExtensions.cs index 1ef8ea6..f85f94f 100644 --- a/DNX.Helpers/Strings/StringExtensions.cs +++ b/DNX.Helpers/Strings/StringExtensions.cs @@ -6,6 +6,8 @@ using System.Text.RegularExpressions; using DNX.Helpers.Linq; +// ReSharper disable InconsistentNaming + // ReSharper disable InvertIf // ReSharper disable LoopCanBeConvertedToQuery @@ -32,6 +34,10 @@ public enum SplitDelimiterType /// public static class StringExtensions { + private const string SI_WORDIFY_REGEX_TEXT = "(?<=[a-z])(?[A-Z])|(?<=.)(?[A-Z])(?=[a-z])"; + + private static readonly Regex WordifyRegex = new Regex(SI_WORDIFY_REGEX_TEXT, RegexOptions.Compiled); + /// /// Formats the specified arguments and text /// @@ -643,5 +649,53 @@ public static bool IsValidNumber(this string text, CultureInfo cultureInfo) return Regex.IsMatch(text, pattern); } + + /// + /// Add spaces to separate the capitalized words in the string, + /// i.e. insert a space before each uppercase letter that is + /// either preceded by a lowercase letter or followed by a + /// lowercase letter (but not for the first char in string). + /// This keeps groups of uppercase letters - e.g. preservedWords - together. + /// + /// A string in PascalCase + /// + public static string Wordify(this string titleCaseString) + { + return titleCaseString.Wordify(null); + } + + /// + /// Add spaces to separate the capitalized words in the string, + /// i.e. insert a space before each uppercase letter that is + /// either preceded by a lowercase letter or followed by a + /// lowercase letter (but not for the first char in string). + /// This keeps groups of uppercase letters - e.g. acronyms - together. + /// + /// A string in PascalCase + /// The preservedWords - beyond acronyms + /// System.String. + public static string Wordify(this string titleCaseString, IList preservedWords) + { + if (string.IsNullOrWhiteSpace(titleCaseString)) + return titleCaseString; + + var text = WordifyRegex + .Replace(titleCaseString, " ${x}") + .Replace(" ", " "); + + if (preservedWords.HasAny()) + { + var acronymDict = preservedWords + .Distinct() + .ToDictionary(x => x.Wordify(), x => x) + ; + + text = acronymDict.Aggregate(text, + (current, dict) => current.Replace(dict.Key, dict.Value) + ); + } + + return text; + } } } diff --git a/Test.DNX.Helpers/Internal/RunSafelyTests.cs b/Test.DNX.Helpers/Internal/RunSafelyTests.cs new file mode 100644 index 0000000..d432daa --- /dev/null +++ b/Test.DNX.Helpers/Internal/RunSafelyTests.cs @@ -0,0 +1,313 @@ +using System; +using System.Threading.Tasks; +using DNX.Helpers.Internal; +using NUnit.Framework; +using Shouldly; + +// ReSharper disable InconsistentNaming + +namespace Test.DNX.Helpers.Internal +{ + public class RunSafelyTests + { + public class Execute_Tests + { + [Test] + public void Can_run_simple_action_that_succeeds() + { + // Arrange + var guid = Guid.NewGuid(); + var value = Guid.Empty; + + // Act + RunSafely.Execute(() => value = guid); + + // Assert + value.ShouldBe(guid); + } + + [Test] + public void Can_handle_simple_action_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + // Act + RunSafely.Execute(() => value = dividend / divisor); + + // Assert + value.ShouldBe(0); + } + + [Test] + public void Can_handle_simple_action_that_fails_and_extract_exception() + { + // Arrange + var value = int.MaxValue; + var guid = Guid.NewGuid(); + var message = ""; + + // Act + RunSafely.Execute(() => throw new Exception(guid.ToString()), ex => message = ex.Message); + + // Assert + value.ShouldBe(int.MaxValue); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain(guid.ToString()); + } + + [Test] + public void Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + // Act + RunSafely.Execute(() => value = dividend / divisor, ex => message = ex.Message); + + // Assert + value.ShouldBe(0); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + } + + public class ExecuteT_Tests + { + [Test] + public void Can_run_simple_func_that_succeeds() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var value = RunSafely.Execute(() => guid); + + // Assert + value.ShouldBe(guid); + } + + [Test] + public void Can_handle_simple_func_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + + // Act + var value = RunSafely.Execute(() => dividend / divisor); + + // Assert + value.ShouldBe(default); + } + + [Test] + public void Can_handle_simple_func_with_default_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, defaultResult); + + // Assert + value.ShouldBe(defaultResult); + } + + [Test] + public void Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var message = ""; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, ex => message = ex.Message); + + // Assert + value.ShouldBe(default(int)); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + + [Test] + public void Can_handle_simple_func_with_default_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var message = ""; + + // Act + var value = RunSafely.Execute(() => dividend / divisor, defaultResult, ex => message = ex.Message); + + // Assert + value.ShouldBe(defaultResult); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + } + + public class ExecuteAsync_Tests + { + [Test] + public async Task Can_run_simple_task_that_succeeds() + { + // Arrange + var dividend = 1000; + var divisor = 20; + var value = 0; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task); + + // Assert + value.ShouldBe(dividend / divisor); + } + + [Test] + public async Task Can_handle_simple_action_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task); + + // Assert + value.ShouldBe(0); + } + + [Test] + public async Task Can_handle_simple_action_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + var task = new Task(() => value = dividend / divisor); + task.Start(); + + // Act + await RunSafely.ExecuteAsync(task, ex => message = ex.Message); + + // Assert + value.ShouldBe(0); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + } + + public class ExecuteAsyncT_Tests + { + private async Task DivideAsync(int dividend, int divisor) + { + var quotient = await Task.Run(() => dividend / divisor); + + return quotient; + } + + [Test] + public async Task Can_run_simple_func_that_succeeds() + { + // Arrange + var dividend = 1000; + var divisor = 50; + + // Act + var value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor)); + + // Assert + value.ShouldBe(dividend / divisor); + } + + [Test] + public async Task Can_run_simple_func_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor)); + + // Assert + value.ShouldBe(default); + } + + [Test] + public async Task Can_run_simple_func_with_default_that_fails() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var value = 0; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), defaultResult); + + // Assert + value.ShouldBe(defaultResult); + } + + [Test] + public async Task Can_handle_simple_func_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var value = 0; + var message = ""; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), ex => message = ex.Message); + + // Assert + value.ShouldBe(default); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + + [Test] + public async Task Can_handle_simple_func_with_default_that_fails_and_extract_exception() + { + // Arrange + var dividend = 1000; + var divisor = 0; + var defaultResult = 500; + var value = 0; + var message = ""; + + // Act + value = await RunSafely.ExecuteAsync(DivideAsync(dividend, divisor), defaultResult, + ex => message = ex.Message); + + // Assert + value.ShouldBe(defaultResult); + message.ShouldNotBeNullOrEmpty(); + message.ShouldContain("divide by zero"); + } + } + } +} diff --git a/Test.DNX.Helpers/Strings/StringExtensionsTests.cs b/Test.DNX.Helpers/Strings/StringExtensionsTests.cs index c6a0bfe..2ab9bc3 100644 --- a/Test.DNX.Helpers/Strings/StringExtensionsTests.cs +++ b/Test.DNX.Helpers/Strings/StringExtensionsTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using DNX.Helpers.Strings; using NUnit.Framework; using Shouldly; @@ -544,5 +545,31 @@ public string Test_CoalesceNullOrWhitespace(string a, string b, string c) return result; } + + [TestCase(null, null, null)] + [TestCase("", null, "")] + [TestCase("TwoWords", null, "Two Words")] + [TestCase("ThreeWordsTogether", null, "Three Words Together")] + [TestCase("pascalCase", null, "pascal Case")] + [TestCase("Already Spaced", null, "Already Spaced")] + [TestCase("AnEntireSentenceSquashedTogetherIntoOneSingleWord", null, "An Entire Sentence Squashed Together Into One Single Word")] + [TestCase("IsITVDramaBetterThanBBCDrama", null, "Is ITV Drama Better Than BBC Drama")] + [TestCase("IsTheHoLAnOutdatedInstitution", null, "Is The Ho L An Outdated Institution")] + [TestCase("IsTheHoLAnOutdatedInstitution", "HoL", "Is The HoL An Outdated Institution")] + [TestCase("VirusesLikeH1N1AndH5N1WereWarningsForThePanDemicToCome", null, "Viruses Like H1N1 And H5N1 Were Warnings For The Pan Demic To Come")] + [TestCase("VirusesLikeH1N1AndH5N1WereWarningsForThePanDemicToCome", "H1N1|H5N1|PanDemic", "Viruses Like H1N1 And H5N1 Were Warnings For The PanDemic To Come")] + public void Wordify_Tests(string text, string preservedWordsText, string expected) + { + // Arrange + var preservedWords = preservedWordsText? + .Split("|") + .ToArray(); + + // Act + var result = text.Wordify(preservedWords); + + // Assert + result.ShouldBe(expected); + } } } diff --git a/Test.DNX.Helpers/Test.DNX.Helpers.csproj b/Test.DNX.Helpers/Test.DNX.Helpers.csproj index 7498d91..2fed06d 100644 --- a/Test.DNX.Helpers/Test.DNX.Helpers.csproj +++ b/Test.DNX.Helpers/Test.DNX.Helpers.csproj @@ -1,6 +1,7 @@  netcoreapp2.1 + latest Martin Smith Tests for DNX.Helpers Copyright © 2016-2019 DNX Solutions Ltd @@ -8,22 +9,25 @@ https://github.com/martinsmith1968/DNX.Helpers favicon-32x32.png - 2.0.8 - 2.0.8 - 2.0.8 + 2.1.0 + 2.1.0 + 2.1.0 - + - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +