From 4466ecf05545a078560258adbe63900a0cc0172b Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 28 Nov 2024 15:17:17 +0100 Subject: [PATCH 1/4] Zwischenstand --- .../EmailValidator.cs | 42 +++++++++++++++++ .../Neolution.Utilities.Validation.csproj | 16 +++++++ .../PhoneNumberHelper.cs | 47 +++++++++++++++++++ Neolution.Utilities/Neolution.Utilities.sln | 25 ++++++++++ 4 files changed, 130 insertions(+) create mode 100644 Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs create mode 100644 Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj create mode 100644 Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs create mode 100644 Neolution.Utilities/Neolution.Utilities.sln diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs b/Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs new file mode 100644 index 0000000..c0d6329 --- /dev/null +++ b/Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs @@ -0,0 +1,42 @@ +namespace Neolution.Utilities.Validation +{ + using System.Net.Mail; + + /// + /// Provides methods to validate email addresses. + /// + public static class EmailValidator + { + /// + /// Determines whether the specified email address is valid. + /// + /// The email address to validate. + /// + /// true if the specified email address is valid; otherwise, false. + /// + public static bool IsEmailValid(string emailAddress) + { + try + { + var mailAddress = new MailAddress(emailAddress); + return !string.IsNullOrWhiteSpace(mailAddress.Address); + } + catch (Exception) + { + return false; + } + } + + /// + /// Determines whether all specified email addresses are valid. + /// + /// A collection of email addresses to validate. + /// + /// true if all email addresses are valid; otherwise, false. + /// + public static bool AreValidEmails(string emailAddresses) + { + return !string.IsNullOrWhiteSpace(emailAddresses) && emailAddresses.Split(",").Select(IsEmailValid).All(isValid => isValid); + } + } +} diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj b/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj new file mode 100644 index 0000000..a0aaad0 --- /dev/null +++ b/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs b/Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs new file mode 100644 index 0000000..d943d7d --- /dev/null +++ b/Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs @@ -0,0 +1,47 @@ +namespace Neolution.Utilities.Validation +{ + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Provides methods for formatting and validating phone numbers. + /// + public static class PhoneNumberHelper + { + /// + /// Formats a phone number by removing spaces and adding the country code if necessary. + /// + /// The phone number to format. + /// + /// The formatted phone number, or an empty string if the input is null or whitespace. + /// + public static string FormatPhoneNumber(string phoneNumber) + { + if (string.IsNullOrWhiteSpace(phoneNumber)) + { + return string.Empty; + } + + var formattedPhoneNumber = phoneNumber.Replace(" ", string.Empty); + + if (formattedPhoneNumber.ElementAt(0) == '0') + { + formattedPhoneNumber = "+41" + formattedPhoneNumber[1..]; + } + + return formattedPhoneNumber; + } + + /// + /// Determines whether the specified string is a valid phone number. + /// + /// The phone number to validate. + /// + /// true if the specified string is a valid phone number; otherwise, false. + /// + public static bool IsPhoneNumber(string phoneNumber) + { + return !string.IsNullOrWhiteSpace(phoneNumber) && Regex.Match(phoneNumber, @"^(\+41|0)\d+$").Success; + } + } +} diff --git a/Neolution.Utilities/Neolution.Utilities.sln b/Neolution.Utilities/Neolution.Utilities.sln new file mode 100644 index 0000000..69002d1 --- /dev/null +++ b/Neolution.Utilities/Neolution.Utilities.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35219.272 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.Utilities.Validation", "Neolution.Utilities.Validation\Neolution.Utilities.Validation.csproj", "{2C418EFC-923C-43D0-B1CA-C6679A9A89DC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8FF12985-C3AB-497B-B2A8-DD02604D5957} + EndGlobalSection +EndGlobal From 8c3f984b1fbff18de9071bd4fda4f6aa9cd85519 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 5 Dec 2024 00:47:08 +0100 Subject: [PATCH 2/4] Added from HBL --- Neolution.Utilities.sln | 39 ++++ .../Auth/ClaimsPrincipalExtensions.cs | 72 +++++++ .../Data/IDataRowExtensions.cs | 61 ++++++ .../DateAndTime/DateTimeExtensions.cs | 101 ++++++++++ Neolution.Utilities/DateAndTime/DateUtils.cs | 179 ++++++++++++++++++ Neolution.Utilities/Files/FileSize.cs | 28 +++ Neolution.Utilities/Files/ZipUtils.cs | 43 +++++ .../Globalization/CultureInfoExtensions.cs | 25 +++ .../Globalization/CultureInfoUtils.cs | 49 +++++ Neolution.Utilities/Linq/ExpressionUtils.cs | 159 ++++++++++++++++ .../Linq/IEnumerableExtensions.cs | 45 +++++ Neolution.Utilities/Linq/IListExtensions.cs | 118 ++++++++++++ .../Linq/IQueryableExtensions.cs | 43 +++++ .../Linq/ReplaceExpressionVisitor.cs | 48 +++++ .../EmailValidator.cs | 2 +- .../Neolution.Utilities.Validation.csproj | 16 -- .../Neolution.Utilities.csproj | 9 + Neolution.Utilities/Neolution.Utilities.sln | 25 --- .../PhoneNumberHelper.cs | 40 ++-- .../Strings/DecimalExtensions.cs | 60 ++++++ .../Strings/StringBuilderExtensions.cs | 26 +++ Neolution.Utilities/Strings/TextUtils.cs | 77 ++++++++ Neolution.Utilities/Uris/UriExtensions.cs | 79 ++++++++ 23 files changed, 1285 insertions(+), 59 deletions(-) create mode 100644 Neolution.Utilities.sln create mode 100644 Neolution.Utilities/Auth/ClaimsPrincipalExtensions.cs create mode 100644 Neolution.Utilities/Data/IDataRowExtensions.cs create mode 100644 Neolution.Utilities/DateAndTime/DateTimeExtensions.cs create mode 100644 Neolution.Utilities/DateAndTime/DateUtils.cs create mode 100644 Neolution.Utilities/Files/FileSize.cs create mode 100644 Neolution.Utilities/Files/ZipUtils.cs create mode 100644 Neolution.Utilities/Globalization/CultureInfoExtensions.cs create mode 100644 Neolution.Utilities/Globalization/CultureInfoUtils.cs create mode 100644 Neolution.Utilities/Linq/ExpressionUtils.cs create mode 100644 Neolution.Utilities/Linq/IEnumerableExtensions.cs create mode 100644 Neolution.Utilities/Linq/IListExtensions.cs create mode 100644 Neolution.Utilities/Linq/IQueryableExtensions.cs create mode 100644 Neolution.Utilities/Linq/ReplaceExpressionVisitor.cs rename Neolution.Utilities/{Neolution.Utilities.Validation => Mail}/EmailValidator.cs (96%) delete mode 100644 Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj create mode 100644 Neolution.Utilities/Neolution.Utilities.csproj delete mode 100644 Neolution.Utilities/Neolution.Utilities.sln rename Neolution.Utilities/{Neolution.Utilities.Validation => Phone}/PhoneNumberHelper.cs (64%) create mode 100644 Neolution.Utilities/Strings/DecimalExtensions.cs create mode 100644 Neolution.Utilities/Strings/StringBuilderExtensions.cs create mode 100644 Neolution.Utilities/Strings/TextUtils.cs create mode 100644 Neolution.Utilities/Uris/UriExtensions.cs diff --git a/Neolution.Utilities.sln b/Neolution.Utilities.sln new file mode 100644 index 0000000..f0e5c54 --- /dev/null +++ b/Neolution.Utilities.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35219.272 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.Utilities", "Neolution.Utilities\Neolution.Utilities.csproj", "{56B6841C-BA77-4A93-A622-24880801C866}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8FFFA751-0701-419F-AD05-74DF0DAF160E}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflow", "workflow", "{50F34C93-AF34-480E-852A-FE3A9B421AFA}" + ProjectSection(SolutionItems) = preProject + .github\workflows\cd-production.yml = .github\workflows\cd-production.yml + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {56B6841C-BA77-4A93-A622-24880801C866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56B6841C-BA77-4A93-A622-24880801C866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56B6841C-BA77-4A93-A622-24880801C866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56B6841C-BA77-4A93-A622-24880801C866}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {50F34C93-AF34-480E-852A-FE3A9B421AFA} = {8FFFA751-0701-419F-AD05-74DF0DAF160E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8FF12985-C3AB-497B-B2A8-DD02604D5957} + EndGlobalSection +EndGlobal diff --git a/Neolution.Utilities/Auth/ClaimsPrincipalExtensions.cs b/Neolution.Utilities/Auth/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..ac4457b --- /dev/null +++ b/Neolution.Utilities/Auth/ClaimsPrincipalExtensions.cs @@ -0,0 +1,72 @@ +namespace Neolution.Utilities.Auth +{ + using System.Globalization; + using System.Security.Claims; + + /// + /// Extensions for ClaimPrincipal / User. + /// + public static class ClaimsPrincipalExtensions + { + /// + /// Users the identifier. + /// + /// The user. + /// The identifier of the user. + public static Guid? UserId(this ClaimsPrincipal user) + { + var value = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + return Guid.TryParse(value, CultureInfo.InvariantCulture, out Guid guid) ? guid : null; + } + + /// + /// Gets the logged-in user identifier. + /// + /// The user. + /// The logged in user identifier + public static Guid LoggedInUserId(this ClaimsPrincipal user) + { + return user?.UserId() ?? throw new ArgumentNullException(nameof(user)); + } + + /// + /// Gets the first name of the user. + /// + /// The user. + /// The first name of the user. + public static string FirstName(this ClaimsPrincipal user) + { + return user?.FindFirst(ClaimTypes.GivenName)?.Value; + } + + /// + /// Gets the last name of the user. + /// + /// The user. + /// The last name of the user. + public static string LastName(this ClaimsPrincipal user) + { + return user?.FindFirst(ClaimTypes.Surname)?.Value; + } + + /// + /// Gets the email of the user. + /// + /// The user. + /// The email of the user. + public static string Email(this ClaimsPrincipal user) + { + return user?.FindFirst(ClaimTypes.Email)?.Value; + } + + /// + /// Gets the username of the user. + /// + /// The user. + /// The username of the user. + public static string UserName(this ClaimsPrincipal user) + { + return user?.FindFirst(ClaimTypes.Name)?.Value; + } + } +} diff --git a/Neolution.Utilities/Data/IDataRowExtensions.cs b/Neolution.Utilities/Data/IDataRowExtensions.cs new file mode 100644 index 0000000..03ec9ea --- /dev/null +++ b/Neolution.Utilities/Data/IDataRowExtensions.cs @@ -0,0 +1,61 @@ +namespace Neolution.Utilities.Data +{ + using System.Data; + using System.Globalization; + + /// + /// The data row extension class + /// + public static class IDataRowExtensions + { + /// + /// Gets the field as decimal. + /// + /// The row. + /// Name of the field. + /// the decimal value + public static decimal GetFieldAsDecimal(this DataRow row, string fieldName) + { + return decimal.Parse(GetFieldValue(row, fieldName), CultureInfo.CurrentCulture); + } + + /// + /// Gets the field as int. + /// + /// The row. + /// Name of the field. + /// the int value + public static int GetFieldAsInt(this DataRow row, string fieldName) + { + return int.Parse(GetFieldValue(row, fieldName), CultureInfo.CurrentCulture); + } + + /// + /// Gets the field as boolean. + /// + /// The row. + /// Name of the field. + /// the boolean value + public static bool GetFieldAsBoolean(this DataRow row, string fieldName) + { + var value = GetFieldValue(row, fieldName); + return !(string.IsNullOrWhiteSpace(value) || value.Trim().ToLowerInvariant() == "0" || value.Trim().ToLowerInvariant() == "false"); + } + + /// + /// Gets the field value. + /// + /// The row. + /// Name of the field. + /// the string value + private static string GetFieldValue(DataRow row, string fieldName) + { + if (string.IsNullOrWhiteSpace(fieldName)) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + return row.Field(fieldName); + } + } +} diff --git a/Neolution.Utilities/DateAndTime/DateTimeExtensions.cs b/Neolution.Utilities/DateAndTime/DateTimeExtensions.cs new file mode 100644 index 0000000..f888c3b --- /dev/null +++ b/Neolution.Utilities/DateAndTime/DateTimeExtensions.cs @@ -0,0 +1,101 @@ +namespace Neolution.Utilities.DateAndTime +{ + using System.Globalization; + + /// + /// Extension Methods for DateTime. + /// + public static class DateTimeExtensions + { + /// + /// Converts the DateTime to the full date time short representation (dddd, dd MMMM yyyy HH:mm) + /// + /// The date time. + /// The full date time short representation + public static string ToFullDateTimeShortString(this DateTime dateTime) + { + return dateTime.ToString("f", CultureInfo.CurrentCulture); + } + + /// + /// Converts the DateTime to the full date time long representation (dddd, dd MMMM yyyy HH:mm:ss) + /// + /// The date time. + /// The full date time short representation + public static string ToFullDateTimeLongString(this DateTime dateTime) + { + return dateTime.ToString("F", CultureInfo.CurrentCulture); + } + + /// + /// Converts the DateTime to the full date time short representation (dd.MM.yyyy HH:mm) + /// + /// The date time. + /// The full date time short representation + public static string ToGeneralDateTimeShortString(this DateTime dateTime) + { + return dateTime.ToString("g", CultureInfo.CurrentCulture); + } + + /// + /// Converts the DateTime to the full date time long representation (dd.MM.yyyy HH:mm:ss) + /// + /// The date time. + /// The full date time short representation + public static string ToGeneralDateTimeLongString(this DateTime dateTime) + { + return dateTime.ToString("G", CultureInfo.CurrentCulture); + } + + /// + /// Gets the date of the Monday of the current week + /// + /// The date time + /// The date + public static DateTime GetFirstDayOfTheWeek(this DateTime dateTime) + { + var difference = (7 + (int)dateTime.DayOfWeek - (int)DayOfWeek.Monday) % 7; + return dateTime.AddDays(-1 * difference).Date; + } + + /// + /// Calculates the days between two dates according to the excel 360 logic. + /// + /// The start date. + /// The end date. + /// The days between two dates according to the excel 360 logic + public static int DateDifferenceInDays360(this DateTime startDate, DateTime endDate) + { + int startDay = startDate.Day; + int startMonth = startDate.Month; + int startYear = startDate.Year; + int endDay = endDate.Day; + int endMonth = endDate.Month; + int endYear = endDate.Year; + + if (startDay == 31 || startDate.IsLastDayOfFebruary()) + { + startDay = 30; + } + + if (startDay == 30 && endDay == 31) + { + endDay = 30; + } + + return ((endYear - startYear) * 360) + ((endMonth - startMonth) * 30) + (endDay - startDay); + } + + /// + /// Determines whether this date is the last day of february. + /// + /// The date. + /// + /// true if last day of february; otherwise, false. + /// + public static bool IsLastDayOfFebruary(this DateTime date) + { + return date.Month == 2 && date.Day == DateTime.DaysInMonth(date.Year, date.Month); + } + } +} diff --git a/Neolution.Utilities/DateAndTime/DateUtils.cs b/Neolution.Utilities/DateAndTime/DateUtils.cs new file mode 100644 index 0000000..b2d1175 --- /dev/null +++ b/Neolution.Utilities/DateAndTime/DateUtils.cs @@ -0,0 +1,179 @@ +namespace Neolution.Utilities.DateAndTime +{ + using DateTime = System.DateTime; + + /// + /// Date utility functions + /// + public static class DateUtils + { + /// + /// Gets the current quarter as an integer in range 1 to 4 + /// + public static int CurrentQuarter { get; } = (int)Math.Ceiling(DateTime.Today.Month / 3d); + + /// + /// Gets the previous quarter as an integer in range 1 to 4 + /// + public static int PreviousQuarter { get; } = (CurrentQuarter + 2) % 4 + 1; + + /// + /// Gets the DateTime of the first day of the current quarter. + /// + /// The date of the first day of the current quarter. + public static DateTime StartOfCurrentQuarter { get; } = new(DateTime.Today.Year, (CurrentQuarter - 1) * 3 + 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Gets the DateTime of the first day of the previous quarter. + /// + /// The date of the first day of the previous quarter. + public static DateTime StartOfPreviousQuarter { get; } = new( + + // if the current quarter is the first one (jan - mar), then the previous quarter lies in the previous year + DateTime.Today.Year - (CurrentQuarter == 1 ? 1 : 0), + + // calculates the starting month of the previous quarter + (PreviousQuarter - 1) * 3 + 1, + 1, + 0, + 0, + 0, + DateTimeKind.Utc); + + /// + /// Combine date and time of two different DateTimes + /// + /// the datetime for date + /// the datetime for time + /// a date + public static DateTime CombineDates(DateTime dateOnly, DateTime? timeOnly) + { + return timeOnly is not null + ? dateOnly.Date.Add(timeOnly.Value.TimeOfDay) + : dateOnly.Date; // TimeOnly is null, reset the time to midnight + } + + /// + /// Method that check if date is at midnight or not + /// + /// the date to check + /// Ignore seconds and milliseconds + /// a bool + public static bool IsMidnight(DateTime date, bool ignoreSeconds) + { + if (ignoreSeconds) + { + return date.Hour == 0 && date.Minute == 0; + } + + return date.TimeOfDay.Ticks == 0; + } + + /// + /// Calculate the difference between a given date and today's date in terms of years + /// Particularly useful for people age calculation + /// + /// the date to compare respect to today's date + /// The difference between today's date and the given date in terms of years + public static int GetYearsFromToday(DateTime date) + { + var today = DateTime.Today; + var years = today.Year - date.Year; + + // Case of a leap year + if (date.Date > today.AddYears(-years)) + { + years--; + } + + return years; + } + + /// + /// Check whether the date is in range (inclusive dates) + /// + /// The date to be checked + /// The inferior bound date + /// The upper bound date + /// Whether the date is in the given range. + public static bool IsDateInRange(DateTime date, DateTime startDate, DateTime endDate) + { + return date.Date >= startDate.Date && date.Date <= endDate.Date; + } + + /// + /// Checks whether the date of birth is in a given range (inclusive dates) + /// + /// The date of birth + /// The inferior bound date + /// The superior bound date + /// A value indicating whether the date of birth is in a given range + public static bool IsDateOfBirthInRange(DateTime dateOfBirth, DateTime startDate, DateTime endDate) + { + var temp = dateOfBirth.AddYears(startDate.Year - dateOfBirth.Year); + + // special case when start date falls in the year before + if (temp < startDate) + { + temp = temp.AddYears(1); + } + + return IsDateInRange(temp, startDate, endDate); + } + + /// + /// Calculate the difference between a two dates in terms of days (endDate - startDate) + /// + /// The end date + /// The start date + /// Indicating whether value should be returned as absolute, hence no matter the dates order. + /// Return (endDate - startDate) in terms of days, possibly in absolute value. + public static int GetDifferenceInDays(DateTime endDate, DateTime startDate, bool absoluteValue) + { + var differenceInDays = (endDate - startDate).Days; + return absoluteValue ? Math.Abs(differenceInDays) : differenceInDays; + } + + /// + /// Determines whether the date is the end of the month + /// + /// The date + /// Whether the date is the end of the month + public static bool IsEndOfMonth(DateTime date) + { + return date.Day == DateTime.DaysInMonth(date.Year, date.Month); + } + + /// + /// Gets the minimum date. + /// + /// The dates. + /// the minimum date + public static DateTime? GetMinimumDate(params DateTime?[] dates) + { + if (dates.Any(x => !x.HasValue)) + { + return null; + } + + return dates.Min(); + } + + /// + /// Gets the minutes until next occurence. + /// + /// The date. + /// The periodicity in minutes. + /// minutes missing to next occurence + public static int GetMinutesUntilNextOccurence(DateTime? date, int periodicityInMinutes) + { + if (!date.HasValue) + { + return 0; + } + + var minutesFromLastOccurence = (int)(DateTime.UtcNow - date.Value).TotalMinutes; + return Math.Max(0, periodicityInMinutes - minutesFromLastOccurence); + } + } +} diff --git a/Neolution.Utilities/Files/FileSize.cs b/Neolution.Utilities/Files/FileSize.cs new file mode 100644 index 0000000..8c1b4a5 --- /dev/null +++ b/Neolution.Utilities/Files/FileSize.cs @@ -0,0 +1,28 @@ +namespace Neolution.Utilities.Files +{ + /// + /// Provides methods to get file sizes in human-readable formats. + /// + public static class FileSize + { + /// + /// Gets the human-readable file size. + /// + /// The size. + /// The size string in human-readable format. + public static string GetHumanReadableFileSize(long size) + { + const int orderDivider = 1024; + var sizes = new[] { "B", "KB", "MB", "GB", "TB" }; + decimal len = size; + int order = 0; + while (len >= orderDivider && order < sizes.Length - 1) + { + order++; + len /= orderDivider; + } + + return $"{len:0.#} {sizes[order]}"; + } + } +} diff --git a/Neolution.Utilities/Files/ZipUtils.cs b/Neolution.Utilities/Files/ZipUtils.cs new file mode 100644 index 0000000..67672bd --- /dev/null +++ b/Neolution.Utilities/Files/ZipUtils.cs @@ -0,0 +1,43 @@ +namespace Neolution.Utilities.Files +{ + using System.IO.Compression; + using System.Text; + + /// + /// Zip utility functions + /// + public static class ZipFile + { + /// + /// Create a zip file starting from a dictotionary of files (name, conent) + /// + /// The files to be processed in the zip. + /// The zip file containing the passed files. + public static Task CreateAsync(Dictionary files) + { + if (files == null) + { + throw new ArgumentNullException(nameof(files)); + } + + return CreateZipFileAsync(files); + + static async Task CreateZipFileAsync(Dictionary files) + { + using var memoryStream = new MemoryStream(); + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create)) + { + foreach (var file in files) + { + var entry = archive.CreateEntry(file.Key); + using var entryStream = entry.Open(); + using var streamWriter = new StreamWriter(entryStream, Encoding.UTF8); + await streamWriter.WriteAsync(file.Value).ConfigureAwait(false); + } + } + + return memoryStream.ToArray(); + } + } + } +} diff --git a/Neolution.Utilities/Globalization/CultureInfoExtensions.cs b/Neolution.Utilities/Globalization/CultureInfoExtensions.cs new file mode 100644 index 0000000..d7a03ab --- /dev/null +++ b/Neolution.Utilities/Globalization/CultureInfoExtensions.cs @@ -0,0 +1,25 @@ +namespace Neolution.Utilities.Globalization +{ + using System.Globalization; + + /// + /// Extension methods for . + /// + public static class CultureInfoExtensions + { + /// + /// Gets the language code + /// + /// The culture information + /// The language code + public static string GetLanguageCode(this CultureInfo cultureInfo) + { + if (cultureInfo == null) + { + throw new ArgumentNullException(nameof(cultureInfo)); + } + + return cultureInfo.IsNeutralCulture ? cultureInfo.Name : cultureInfo.Parent.Name; + } + } +} \ No newline at end of file diff --git a/Neolution.Utilities/Globalization/CultureInfoUtils.cs b/Neolution.Utilities/Globalization/CultureInfoUtils.cs new file mode 100644 index 0000000..9484bf1 --- /dev/null +++ b/Neolution.Utilities/Globalization/CultureInfoUtils.cs @@ -0,0 +1,49 @@ +namespace Neolution.Utilities.Globalization +{ + using System.Globalization; + + /// + /// CultureInfo utility functions + /// + public static class CultureInfoCreator + { + /// + /// Creates a culture. + /// + /// The name of the culture. + /// The culture info. + public static CultureInfo CreateWithSwissFormat(string name) + { + const int currencyDecimalDigits = 0; + const int numberDecimalDigits = 2; + const int percentDecimalDigits = 3; + + return new CultureInfo(name) + { + NumberFormat = + { + CurrencySymbol = string.Empty, + CurrencyDecimalDigits = currencyDecimalDigits, + CurrencyPositivePattern = 0, // https://learn.microsoft.com/en-us/dotnet/api/system.globalization.numberformatinfo.currencypositivepattern#remarks + CurrencyNegativePattern = 1, // https://learn.microsoft.com/en-us/dotnet/api/system.globalization.numberformatinfo.currencynegativepattern#remarks + CurrencyDecimalSeparator = ".", + CurrencyGroupSeparator = "'", + NumberDecimalDigits = numberDecimalDigits, + NumberDecimalSeparator = ".", + NumberGroupSeparator = "'", + PercentDecimalDigits = percentDecimalDigits, + PercentDecimalSeparator = ".", + PercentGroupSeparator = "'", + }, + DateTimeFormat = + { + DateSeparator = ".", + ShortDatePattern = "dd.MM.yyyy", + LongDatePattern = "dddd, dd MMMM yyyy", + ShortTimePattern = "HH:mm", + LongTimePattern = "HH:mm:ss", + }, + }; + } + } +} diff --git a/Neolution.Utilities/Linq/ExpressionUtils.cs b/Neolution.Utilities/Linq/ExpressionUtils.cs new file mode 100644 index 0000000..2e94d96 --- /dev/null +++ b/Neolution.Utilities/Linq/ExpressionUtils.cs @@ -0,0 +1,159 @@ +namespace Neolution.Utilities.Linq +{ + using System.Linq.Expressions; + + /// + /// Some expressions utilities. + /// + public static class ExpressionUtils + { + /// + /// Gets the path of an expression. + /// + /// The source type. + /// The path. + /// A string with all properties separated by a dot. + public static string PathOf(Expression> expression) + { + if (expression == null) + { + throw new ArgumentNullException(nameof(expression)); + } + + Stack memberNames = new Stack(); + + MemberExpression memberExp = GetMemberExpression(expression.Body); + + while (memberExp != null) + { + memberNames.Push(memberExp.Member.Name); + memberExp = GetMemberExpression(memberExp.Expression); + } + + return string.Join(".", memberNames); + } + + /// + /// Gets the full path of an expression. + /// + /// The source type. + /// The path. + /// A string with all properties separated by a dot. + public static string FullPathOf(Expression> expression) + { + return $"{typeof(T).Name}.{PathOf(expression)}"; + } + + /// + /// Gets the member expression. + /// + /// The expression to unwrap. + /// The member expression. + public static MemberExpression GetMemberExpression(Expression toUnwrap) + { + if (toUnwrap is UnaryExpression unaryExpression) + { + return unaryExpression.Operand as MemberExpression; + } + + return toUnwrap as MemberExpression; + } + + /// + /// Connects two expressions by an or condition. + /// + /// The of object. + /// The expr1. + /// The expr2. + /// Th expression. + /// + /// expr1 + /// or + /// expr2. + /// + public static Expression> Or(Expression> expr1, Expression> expr2) + { + if (expr1 == null) + { + throw new ArgumentNullException(nameof(expr1)); + } + + if (expr2 == null) + { + throw new ArgumentNullException(nameof(expr2)); + } + + var parameter = Expression.Parameter(typeof(T)); + + var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); + var left = leftVisitor.Visit(expr1.Body); + + var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); + var right = rightVisitor.Visit(expr2.Body); + + return Expression.Lambda>(Expression.Or(left, right), parameter); + } + + /// + /// Connects two expressions by an and condition. + /// + /// The of object. + /// The expr1. + /// The expr2. + /// Th expression. + /// + /// expr1 + /// and + /// expr2. + /// + public static Expression> And(Expression> expr1, Expression> expr2) + { + if (expr1 == null) + { + throw new ArgumentNullException(nameof(expr1)); + } + + if (expr2 == null) + { + throw new ArgumentNullException(nameof(expr2)); + } + + var parameter = Expression.Parameter(typeof(T)); + + var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); + var left = leftVisitor.Visit(expr1.Body); + + var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); + var right = rightVisitor.Visit(expr2.Body); + + return Expression.Lambda>(Expression.And(left, right), parameter); + } + + /// + /// Evaluates the comparison. + /// + /// Type of arguments. + /// The first argument. + /// The second argument. + /// The sign. + /// True or false of comparison + public static bool EvaluateComparison(T a, T b, string sign) + { + var arg1 = Expression.Parameter(typeof(T)); + var arg2 = Expression.Parameter(typeof(T)); + + var comparison = sign switch + { + "<" => Expression.LessThan(arg1, arg2), + "<=" => Expression.LessThanOrEqual(arg1, arg2), + ">" => Expression.GreaterThan(arg1, arg2), + ">=" => Expression.GreaterThanOrEqual(arg1, arg2), + _ => throw new NotSupportedException("Operation is unknown"), + }; + + var func = Expression.Lambda>(comparison, arg1, arg2).Compile(); + + return func(a, b); + } + } +} diff --git a/Neolution.Utilities/Linq/IEnumerableExtensions.cs b/Neolution.Utilities/Linq/IEnumerableExtensions.cs new file mode 100644 index 0000000..1cd65c7 --- /dev/null +++ b/Neolution.Utilities/Linq/IEnumerableExtensions.cs @@ -0,0 +1,45 @@ +namespace Neolution.Utilities.Linq +{ + /// + /// IEnumerable extensions + /// + public static class IEnumerableExtensions + { + /// + /// Performs the specified action on each element of the list. + /// + /// The item type + /// The items + /// The action + public static void ForEach(this IEnumerable items, Action action) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + foreach (var item in items) + { + action(item); + } + } + + /// + /// FirstOrDefault for struct types. + /// + /// The struct type. + /// The items. + /// The predicate. + /// The FirstOrDefault + public static T? StructFirstOrDefault(this IEnumerable items, Func predicate) + where T : struct + { + return items.Where(predicate).Cast().FirstOrDefault(); + } + } +} diff --git a/Neolution.Utilities/Linq/IListExtensions.cs b/Neolution.Utilities/Linq/IListExtensions.cs new file mode 100644 index 0000000..a57bc97 --- /dev/null +++ b/Neolution.Utilities/Linq/IListExtensions.cs @@ -0,0 +1,118 @@ +namespace Neolution.Utilities.Linq +{ + /// + /// IList extensions + /// + public static class IListExtensions + { + /// + /// Adds the range. + /// + /// The list type + /// The list + /// The items + public static void AddRange(this IList list, IEnumerable items) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (list is List asList) + { + asList.AddRange(items); + } + else + { + foreach (var item in items) + { + list.Add(item); + } + } + } + + /// + /// Removes the range. + /// + /// the list type + /// The list. + /// The items. + public static void RemoveRange(this IList list, IEnumerable items) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + // Evaluate the enumerable because items is usually a subset of list, + // This prevents 'Collection was modified' error + foreach (var item in items.ToList()) + { + list.Remove(item); + } + } + + /// + /// Performs the distinct with a key selector. + /// + /// The type of the source. + /// The type of the key. + /// The list + /// The key selector + /// The distinct list + public static IList DistinctBy(this IList list, Func keySelector) + { + return list.DistinctBy(keySelector, x => x.First()); + } + + /// + /// Performs the distinct with a key selector. + /// + /// The type of the source. + /// The type of the key. + /// The list + /// The key selector + /// The group selector + /// The distinct list + public static IList DistinctBy(this IList list, Func keySelector, Func, TSource> groupSelector) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + return list.GroupBy(keySelector).Select(x => groupSelector(x)).ToList(); + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// The list type + /// The list + /// The delegate that defines the conditions of the elements to remove. + public static void RemoveAll(this IList list, Predicate match) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (match == null) + { + throw new ArgumentNullException(nameof(match)); + } + + list.RemoveRange(list.Where(x => match(x))); + } + } +} diff --git a/Neolution.Utilities/Linq/IQueryableExtensions.cs b/Neolution.Utilities/Linq/IQueryableExtensions.cs new file mode 100644 index 0000000..bfc0617 --- /dev/null +++ b/Neolution.Utilities/Linq/IQueryableExtensions.cs @@ -0,0 +1,43 @@ +namespace Neolution.Utilities.Linq +{ + using System.ComponentModel; + using System.Linq.Expressions; + + /// + /// Extensions for the IQueryable interface. + /// + public static class IQueryableExtensions + { + /// + /// Sorts the elements of a sequence according to a key and order direction. + /// + /// The type of the source. + /// The type of the key. + /// The query. + /// The key selector. + /// The sort direction. + /// The ordered query. + public static IOrderedQueryable OrderBy(this IQueryable query, Expression> keySelector, ListSortDirection sortDirection) + { + var ascending = sortDirection == ListSortDirection.Ascending; + + return ascending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector); + } + + /// + /// Sorts the elements of a sequence according to a key and order direction. + /// + /// The type of the source. + /// The type of the key. + /// The query. + /// The key selector. + /// The sort direction. + /// The ordered query. + public static IOrderedQueryable ThenBy(this IOrderedQueryable query, Expression> keySelector, ListSortDirection sortDirection) + { + var ascending = sortDirection == ListSortDirection.Ascending; + + return ascending ? query.ThenBy(keySelector) : query.ThenByDescending(keySelector); + } + } +} diff --git a/Neolution.Utilities/Linq/ReplaceExpressionVisitor.cs b/Neolution.Utilities/Linq/ReplaceExpressionVisitor.cs new file mode 100644 index 0000000..96b46e9 --- /dev/null +++ b/Neolution.Utilities/Linq/ReplaceExpressionVisitor.cs @@ -0,0 +1,48 @@ +namespace Neolution.Utilities.Linq +{ + using System.Linq.Expressions; + + /// + /// Internal helper class. + /// + internal class ReplaceExpressionVisitor : ExpressionVisitor + { + /// + /// The old value. + /// + private readonly Expression oldValue; + + /// + /// The new value. + /// + private readonly Expression newValue; + + /// + /// Initializes a new instance of the class. + /// + /// The old value. + /// The new value. + public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) + { + this.oldValue = oldValue; + this.newValue = newValue; + } + + /// + /// Dispatches the expression to one of the more specialized visit methods in this class. + /// + /// The expression to visit. + /// + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + /// + public override Expression Visit(Expression node) + { + if (node == oldValue) + { + return newValue; + } + + return base.Visit(node); + } + } +} diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs b/Neolution.Utilities/Mail/EmailValidator.cs similarity index 96% rename from Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs rename to Neolution.Utilities/Mail/EmailValidator.cs index c0d6329..7c06e63 100644 --- a/Neolution.Utilities/Neolution.Utilities.Validation/EmailValidator.cs +++ b/Neolution.Utilities/Mail/EmailValidator.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities.Validation +namespace Neolution.Utilities.Mail { using System.Net.Mail; diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj b/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj deleted file mode 100644 index a0aaad0..0000000 --- a/Neolution.Utilities/Neolution.Utilities.Validation/Neolution.Utilities.Validation.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net8.0 - enable - enable - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/Neolution.Utilities/Neolution.Utilities.csproj b/Neolution.Utilities/Neolution.Utilities.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Neolution.Utilities/Neolution.Utilities.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Neolution.Utilities/Neolution.Utilities.sln b/Neolution.Utilities/Neolution.Utilities.sln deleted file mode 100644 index 69002d1..0000000 --- a/Neolution.Utilities/Neolution.Utilities.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35219.272 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.Utilities.Validation", "Neolution.Utilities.Validation\Neolution.Utilities.Validation.csproj", "{2C418EFC-923C-43D0-B1CA-C6679A9A89DC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C418EFC-923C-43D0-B1CA-C6679A9A89DC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {8FF12985-C3AB-497B-B2A8-DD02604D5957} - EndGlobalSection -EndGlobal diff --git a/Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs b/Neolution.Utilities/Phone/PhoneNumberHelper.cs similarity index 64% rename from Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs rename to Neolution.Utilities/Phone/PhoneNumberHelper.cs index d943d7d..14c1414 100644 --- a/Neolution.Utilities/Neolution.Utilities.Validation/PhoneNumberHelper.cs +++ b/Neolution.Utilities/Phone/PhoneNumberHelper.cs @@ -1,21 +1,39 @@ -namespace Neolution.Utilities.Validation +namespace Neolution.Utilities.Phone { using System.Linq; using System.Text.RegularExpressions; /// - /// Provides methods for formatting and validating phone numbers. + /// Provides methods for validating phone numbers. /// - public static class PhoneNumberHelper + public static class PhoneNumberValidator { /// - /// Formats a phone number by removing spaces and adding the country code if necessary. + /// Determines whether the specified string is a valid Swiss phone number. + /// + /// The phone number to validate. + /// + /// true if the specified string is a valid Swiss phone number; otherwise, false. + /// + public static bool IsSwissPhoneNumber(string phoneNumber) + { + return !string.IsNullOrWhiteSpace(phoneNumber) && Regex.Match(phoneNumber, @"^(\+41|0)\d+$").Success; + } + } + + /// + /// Provides methods for formatting phone numbers. + /// + public static class PhoneNumberFormatter + { + /// + /// Formats a phone number by removing spaces and adding the Swiss country code if necessary. /// /// The phone number to format. /// /// The formatted phone number, or an empty string if the input is null or whitespace. /// - public static string FormatPhoneNumber(string phoneNumber) + public static string FormatSwissPhoneNumber(string phoneNumber) { if (string.IsNullOrWhiteSpace(phoneNumber)) { @@ -31,17 +49,5 @@ public static string FormatPhoneNumber(string phoneNumber) return formattedPhoneNumber; } - - /// - /// Determines whether the specified string is a valid phone number. - /// - /// The phone number to validate. - /// - /// true if the specified string is a valid phone number; otherwise, false. - /// - public static bool IsPhoneNumber(string phoneNumber) - { - return !string.IsNullOrWhiteSpace(phoneNumber) && Regex.Match(phoneNumber, @"^(\+41|0)\d+$").Success; - } } } diff --git a/Neolution.Utilities/Strings/DecimalExtensions.cs b/Neolution.Utilities/Strings/DecimalExtensions.cs new file mode 100644 index 0000000..b033d1d --- /dev/null +++ b/Neolution.Utilities/Strings/DecimalExtensions.cs @@ -0,0 +1,60 @@ +namespace Neolution.Utilities.Strings +{ + using System.Globalization; + + /// + /// The decimal extensions + /// + public static class DecimalExtensions + { + /// + /// The long percent format + /// + public static readonly string LongPercentFormat = "#,##0.000#"; + + /// + /// The signed long percent format + /// + public static readonly string SignedLongPercentFormat = $"+{LongPercentFormat};-{LongPercentFormat};{LongPercentFormat}"; + + /// + /// Converts to long percent string. + /// + /// The value. + /// The value as long percent string + public static string ToLongPercentString(this decimal value) + { + return value.ToString(LongPercentFormat, CultureInfo.CurrentCulture); + } + + /// + /// Converts to long percent string. + /// + /// The value. + /// The value as long percent string + public static string ToLongPercentString(this decimal? value) + { + return value.HasValue ? value.Value.ToLongPercentString() : null; + } + + /// + /// Converts to long percent string with sign (+-). + /// + /// The value. + /// The value as long percent string + public static string ToSignedLongPercentString(this decimal value) + { + return value.ToString(SignedLongPercentFormat, CultureInfo.CurrentCulture); + } + + /// + /// Converts to signedcurrencystring. + /// + /// The value. + /// the signed currency value as string + public static string ToSignedCurrencyString(this decimal value) + { + return value > 0 ? $"+{value:C}" : $"{value:C}"; + } + } +} diff --git a/Neolution.Utilities/Strings/StringBuilderExtensions.cs b/Neolution.Utilities/Strings/StringBuilderExtensions.cs new file mode 100644 index 0000000..76e8cf1 --- /dev/null +++ b/Neolution.Utilities/Strings/StringBuilderExtensions.cs @@ -0,0 +1,26 @@ +namespace Neolution.Utilities.Strings +{ + using System.Text; + + /// + /// StringBuilder Extensions + /// + public static class StringBuilderExtensions + { + /// + /// Appends the line with the specified padding + /// + /// The string builder + /// The value + /// The padding + public static void AppendLine(this StringBuilder stringBuilder, string value, int padding) + { + if (stringBuilder == null) + { + throw new ArgumentNullException(nameof(stringBuilder)); + } + + stringBuilder.AppendLine($"{new string(' ', padding)}{value}"); + } + } +} diff --git a/Neolution.Utilities/Strings/TextUtils.cs b/Neolution.Utilities/Strings/TextUtils.cs new file mode 100644 index 0000000..ab7e09e --- /dev/null +++ b/Neolution.Utilities/Strings/TextUtils.cs @@ -0,0 +1,77 @@ +namespace Neolution.Utilities.Strings +{ + using System.Text.RegularExpressions; + + /// + /// Text utility functions + /// + public static class TextUtils + { + /// + /// Converts the new line characters to
tags. + ///
+ /// The text to convert + /// The converted text + public static string ConvertNewLineToBr(string text) + { + if (string.IsNullOrEmpty(text)) + { + return text; + } + + return Regex.Replace(text, "\\r?\\n{1}", "
"); + } + + /// + /// Extracts the GUID from a string. + /// + /// The text + /// The extracted GUID + public static string ExtractGuidFromString(string text) + { + return ExtractGuidsFromString(text).FirstOrDefault(); + } + + /// + /// Extracts the GUIDs from a string. + /// + /// The text + /// The extracted GUIDs + public static IList ExtractGuidsFromString(string text) + { + const string guidRegex = "([a-zA-Z0-9]{8}\\-[a-zA-Z0-9]{4}\\-[a-zA-Z0-9]{4}\\-[a-zA-Z0-9]{4}\\-[a-zA-Z0-9]{12})"; + return Regex.Matches(text, guidRegex).AsEnumerable().SelectMany(x => x.Captures).Select(x => x.Value).ToList(); + } + + /// + /// Truncates the specified text. + /// + /// The text. + /// The length. + /// The truncated text. + public static string Truncate(string text, int length) + { + if (string.IsNullOrEmpty(text)) + { + return text; + } + + return text.Substring(0, Math.Min(text.Length, length)); + } + + /// + /// Replaces the invalid file name characters. + /// + /// The filename. + /// The filename with the invalid characters replaced + public static string ReplaceInvalidFileNameChars(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + return filename; + } + + return string.Join("_", filename.Split(Path.GetInvalidFileNameChars())); + } + } +} diff --git a/Neolution.Utilities/Uris/UriExtensions.cs b/Neolution.Utilities/Uris/UriExtensions.cs new file mode 100644 index 0000000..5cd9e2c --- /dev/null +++ b/Neolution.Utilities/Uris/UriExtensions.cs @@ -0,0 +1,79 @@ +namespace Neolution.Utilities.Uris +{ + using System.Globalization; + using System.Web; + + /// + /// Extension methods for Uri + /// + public static class UriExtensions + { + /// + /// Appends the specified paths. + /// + /// The URI. + /// The paths. + /// The URI with the appended paths + public static Uri Append(this Uri uri, params string[] paths) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => string.Format(CultureInfo.InvariantCulture, "{0}/{1}", current.TrimEnd('/'), path.TrimStart('/')))); + } + + /// + /// Adds the query parameter. + /// + /// The URI. + /// The name. + /// The value. + /// the uri with appended query parameter + public static Uri AddQueryParameter(this Uri uri, string name, string value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(nameof(value)); + } + + var httpValueCollection = HttpUtility.ParseQueryString(uri.Query); + + httpValueCollection.Remove(name); + httpValueCollection.Add(name, value); + + var uriBuilder = new UriBuilder(uri); + uriBuilder.Query = httpValueCollection.ToString(); + + return uriBuilder.Uri; + } + + /// + /// Adds the query parameter. + /// + /// The URI. + /// The name. + /// The value. + /// the uri with appended query parameter + public static Uri AddQueryParameter(this Uri uri, string name, object value) + { + return uri.AddQueryParameter(name, $"{value}"); + } + } +} From acf26c6f9a7bbceb9e293baf3ae4572bd259db8d Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 5 Dec 2024 14:35:32 +0100 Subject: [PATCH 3/4] Add Github actions files --- .github/workflows/cd-production.yml | 40 ++++++++++++++ .github/workflows/ci.yml | 32 ++++++++++++ .github/workflows/create-release.yml | 78 ++++++++++++++++++++++++++++ .github/workflows/notify-slack.yml | 16 ++++++ .release-it.json | 23 ++++++++ CHANGELOG.md | 15 ++++++ LICENSE | 2 +- Neolution.Utilities.sln | 1 + README.md | 16 +++++- 9 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/cd-production.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/notify-slack.yml create mode 100644 .release-it.json create mode 100644 CHANGELOG.md diff --git a/.github/workflows/cd-production.yml b/.github/workflows/cd-production.yml new file mode 100644 index 0000000..f2c4f6b --- /dev/null +++ b/.github/workflows/cd-production.yml @@ -0,0 +1,40 @@ +name: Publish NuGet package + +on: + release: + types: [published] + +env: + ARTIFACTS_FEED_URL: https://api.nuget.org/v3/index.json + PROJECT_NAME: "Neolution.DotNet.Console" + BUILD_CONFIGURATION: "Release" + DOTNET_VERSION: "8.x" + +jobs: + build-pack-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + source-url: ${{ env.ARTIFACTS_FEED_URL }} + env: + NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY_NEOLUTION }} + + - name: Determine version for NuGet package + run: echo "NUGET_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + + - name: Build and pack + run: | + dotnet restore + dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} -p:Version=$NUGET_VERSION + dotnet pack "${{ env.PROJECT_NAME }}\${{ env.PROJECT_NAME }}.csproj" --configuration ${{ env.BUILD_CONFIGURATION }} --no-build -p:PackageVersion=$NUGET_VERSION + + - name: Push NuGet package + run: dotnet nuget push --skip-duplicate -k $NUGET_AUTH_TOKEN **/bin/${{ env.BUILD_CONFIGURATION }}/*.nupkg + env: + NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY_NEOLUTION }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6210625 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + BUILD_CONFIGURATION: "Release" + DOTNET_VERSION: "8.x" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration '${{ env.BUILD_CONFIGURATION }}' + + - name: Test + run: dotnet test --no-build --verbosity normal --configuration '${{ env.BUILD_CONFIGURATION }}' diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..3a283d7 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,78 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + versioning_phase: + type: choice + description: Versioning Phase + default: stable + options: + - alpha + - beta + - rc + - stable + + bump_version_number: + type: choice + description: Bump Version Number + default: consecutive + options: + - consecutive + - patch + - minor + - major + + is_dry_run: + type: boolean + description: Dry Run + +jobs: + release-it: + runs-on: ubuntu-latest + steps: + - uses: tibdex/github-app-token@v2 + id: generate-token + with: + app_id: ${{ secrets.RELEASE_BOT_APP_ID }} + private_key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }} + + - name: checkout + uses: actions/checkout@v4 + with: + token: ${{ steps.generate-token.outputs.token }} + # we need everything so release-it can compare the current version with the latest tag + fetch-depth: 0 + + - name: initialize mandatory git config + run: | + git config user.name "GitHub Release Bot" + git config user.email release-bot@neolution.ch + + - name: install release-it with plugins + run: npm install -g release-it @release-it/keep-a-changelog + + - name: run release-it + run: | + params=() + + if [[ ${{ github.event.inputs.bump_version_number }} != "consecutive" ]]; then + params+=(${{ github.event.inputs.bump_version_number }}) + fi + + if [[ ${{ github.event.inputs.versioning_phase }} != "stable" ]]; then + params+=(--preRelease=${{ github.event.inputs.versioning_phase }}) + params+=(--plugins.@release-it/keep-a-changelog.keepUnreleased) + params+=(--no-plugins.@release-it/keep-a-changelog.strictLatest) + fi + + if [[ ${{ github.event.inputs.is_dry_run }} == "true" ]]; then + params+=(--dry-run) + fi + + params+=(--ci) + + echo "command: release-it ${params[@]}" + release-it "${params[@]}" + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/notify-slack.yml b/.github/workflows/notify-slack.yml new file mode 100644 index 0000000..42311bc --- /dev/null +++ b/.github/workflows/notify-slack.yml @@ -0,0 +1,16 @@ +name: Notify Slack Channel + +on: + release: + types: [published] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - uses: neolution-ch/action-release-notifier@v1 + with: + slack-token: ${{ secrets.SLACK_RELEASE_NOTIFIER_TOKEN }} + slack-channel-ids: ${{ vars.SLACK_CHANNEL_ID_RELEASE_ANNOUNCEMENTS }} + ignore-alpha-releases: true + ignore-rc-releases: true diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 0000000..cebc73d --- /dev/null +++ b/.release-it.json @@ -0,0 +1,23 @@ +{ + "git": { + "push": true + }, + "npm": { + "publish": false, + "skipChecks": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/keep-a-changelog": { + "filename": "CHANGELOG.md", + "addVersionUrl": true, + "addUnreleased": true, + "strictLatest": true + } + }, + "hooks": { + "before:git:release": "npx prettier -y --write CHANGELOG.md && git add CHANGELOG.md" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..585e358 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- This CHANGELOG file to document changes to the project. +- Basic project structure and utility classes. + + diff --git a/LICENSE b/LICENSE index f0bb4ec..2b8f278 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Neolution AG +Copyright (c) 2022 Neolution AG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Neolution.Utilities.sln b/Neolution.Utilities.sln index f0e5c54..493ad98 100644 --- a/Neolution.Utilities.sln +++ b/Neolution.Utilities.sln @@ -7,6 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.Utilities", "Neol EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8FFFA751-0701-419F-AD05-74DF0DAF160E}" ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md README.md = README.md EndProjectSection EndProject diff --git a/README.md b/README.md index 0929741..b1eeea1 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# Neolution.Utilities \ No newline at end of file +# Neolution.Utilities + +## Description +This is a collection of utilities that we have created to help with our development process. We have decided to open source these utilities in the hopes that they will be useful to others. + +## Usage +To use these utilities, simply add the Nuget package to your project and use the desired classes in your source code. + +## Contributing +If you would like to contribute to this project, please submit a pull request. + +## Releases +Due to the nature of this project as a loosely connected collection of utilities, it's important to be strict about following [SemVer](https://semver.org/) to communicate possible breaking changes to the users of this library via version number. + +Equally as important is to be precise about the changes made in each release and maintain the [CHANGELOG.md](CHANGELOG.md) according to the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format so users of this library can see easily where changes were made. \ No newline at end of file From 128e8b6450a54cfe01106b01334413bd94f0249f Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 5 Dec 2024 15:05:47 +0100 Subject: [PATCH 4/4] fix description --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 585e358..14d2663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - This CHANGELOG file to document changes to the project. -- Basic project structure and utility classes. +- Basic project structure and first few utility classes.