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..14d2663 --- /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 first few 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 new file mode 100644 index 0000000..493ad98 --- /dev/null +++ b/Neolution.Utilities.sln @@ -0,0 +1,40 @@ + +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 + CHANGELOG.md = CHANGELOG.md + 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/Mail/EmailValidator.cs b/Neolution.Utilities/Mail/EmailValidator.cs new file mode 100644 index 0000000..7c06e63 --- /dev/null +++ b/Neolution.Utilities/Mail/EmailValidator.cs @@ -0,0 +1,42 @@ +namespace Neolution.Utilities.Mail +{ + 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.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/Phone/PhoneNumberHelper.cs b/Neolution.Utilities/Phone/PhoneNumberHelper.cs new file mode 100644 index 0000000..14c1414 --- /dev/null +++ b/Neolution.Utilities/Phone/PhoneNumberHelper.cs @@ -0,0 +1,53 @@ +namespace Neolution.Utilities.Phone +{ + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Provides methods for validating phone numbers. + /// + public static class PhoneNumberValidator + { + /// + /// 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 FormatSwissPhoneNumber(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; + } + } +} 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}"); + } + } +} 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