From 5f5b32666858ec42635efa6fb14542bdbdad09ee Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Tue, 24 Jan 2023 13:08:24 -0300 Subject: [PATCH] Add Phone number model --- src/Cpf.cs | 2 +- src/CpfCnpj.cs | 2 +- src/Extensions.cs | 4 +- src/PhoneNumber.cs | 94 ++++++++++++++++++++ tests/BrazilModels.Tests/PhoneNumberTests.cs | 71 +++++++++++++++ 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/PhoneNumber.cs create mode 100644 tests/BrazilModels.Tests/PhoneNumberTests.cs diff --git a/src/Cpf.cs b/src/Cpf.cs index efa851e..2b33258 100644 --- a/src/Cpf.cs +++ b/src/Cpf.cs @@ -216,7 +216,7 @@ public static bool Validate(in ReadOnlySpan cpfString) /// - /// Format Cpnj string. + /// Format Cpf string. /// If has size smaller then expected, this function will pad the value with left 0. /// /// Cpf string representation diff --git a/src/CpfCnpj.cs b/src/CpfCnpj.cs index dbbfdcf..f13ea89 100644 --- a/src/CpfCnpj.cs +++ b/src/CpfCnpj.cs @@ -198,7 +198,7 @@ string DebuggerDisplay() => Value == Empty /// true if the validation was successful; otherwise, false. public static DocumentType? Validate(in ReadOnlySpan cpfOrCnpj) { - var cleared = cpfOrCnpj.ClearString(); + var cleared = cpfOrCnpj.RemoveNonDigits(); return cleared.Length switch { Cnpj.DefaultLength when Cnpj.Validate(cleared) => DocumentType.CNPJ, diff --git a/src/Extensions.cs b/src/Extensions.cs index 5980344..d6ca8c2 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -86,7 +86,7 @@ public static string ToBrazilMoneyString(this decimal value, bool moneySuffix = static class Extensions { - public static ReadOnlySpan ClearString(in this ReadOnlySpan value) => + public static ReadOnlySpan RemoveNonDigits(in this ReadOnlySpan value) => Regex.Replace(value.ToString(), "[^0-9a-zA-Z]+", string.Empty).AsSpan().Trim(); public static ReadOnlySpan FormatMask(in this ReadOnlySpan value, int size, @@ -99,7 +99,7 @@ public static ReadOnlySpan FormatMask(in this ReadOnlySpan value, in public static ReadOnlySpan FormatClean(in this ReadOnlySpan value, int size) => value.IsEmptyOrWhiteSpace() ? string.Empty - : value.ClearString().PadZero(size); + : value.RemoveNonDigits().PadZero(size); public static ReadOnlySpan PadZero(in this ReadOnlySpan value, int totalWidth) { diff --git a/src/PhoneNumber.cs b/src/PhoneNumber.cs new file mode 100644 index 0000000..6bfbf9b --- /dev/null +++ b/src/PhoneNumber.cs @@ -0,0 +1,94 @@ +namespace BrazilModels; + +using System; +using System.ComponentModel; +using System.Diagnostics; + +/// +/// Basic strongly typed Phone number representation +/// +[System.Text.Json.Serialization.JsonConverter(typeof(StringSystemTextJsonConverter))] +[TypeConverter(typeof(StringTypeConverter))] +[DebuggerDisplay("{DebuggerDisplay(),nq}")] +[Swashbuckle.AspNetCore.Annotations.SwaggerSchemaFilter(typeof(StringSchemaFilter))] +public readonly record struct PhoneNumber : IComparable +{ + /// + /// String representation of the Phone number + /// + public string Value { get; } + + /// + /// Create a new phone number instance + /// + /// + /// + public PhoneNumber(string phoneNumber) + { + ArgumentNullException.ThrowIfNull(phoneNumber); + this.Value = Format(phoneNumber.ToLowerInvariant()); + } + + /// + public override string ToString() => Value; + + /// + /// Get phoneNumber instance of an Value string + /// + /// + /// + public static explicit operator PhoneNumber(string value) + => Parse(value); + + /// + /// Return string representation of phoneNumber + /// + public static implicit operator string(PhoneNumber phoneNumber) + => phoneNumber.Value; + + /// + /// Try parse an Value string to an phoneNumber instance + /// + public static bool TryParse(string? value, out PhoneNumber phoneNumber) + { + phoneNumber = default; + if (string.IsNullOrWhiteSpace(value)) + return false; + + phoneNumber = new(value); + return true; + } + + /// + /// Parse an Value string to an phoneNumber instance + /// + /// + /// + public static PhoneNumber Parse(string value) => + TryParse(value, out var valid) + ? valid + : throw new InvalidOperationException($"Invalid phoneNumber {value}"); + + /// + public int CompareTo(PhoneNumber other) => + string.Compare(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + string DebuggerDisplay() => $"PHONE{{{Value}}}"; + + /// + /// Format Phone string. + /// + /// Phone string representation + /// Formatted Phone string + public static string Format(in ReadOnlySpan value) + { + if (value.IsEmptyOrWhiteSpace()) + return string.Empty; + + var clean = value.RemoveNonDigits(); + if (value.StartsWith("+")) + return "+" + clean.ToString(); + + return clean.ToString(); + } +} diff --git a/tests/BrazilModels.Tests/PhoneNumberTests.cs b/tests/BrazilModels.Tests/PhoneNumberTests.cs new file mode 100644 index 0000000..a2cdc6b --- /dev/null +++ b/tests/BrazilModels.Tests/PhoneNumberTests.cs @@ -0,0 +1,71 @@ +using System.Text.RegularExpressions; + +namespace BrazilModels.Tests; + +using System.Text.Json; + +public class PhoneNumberTests : BaseTest +{ + [Test] + public void ShouldReturnValidForValidPhoneNumber() + { + var phoneNumberStr = faker.Person.Phone; + PhoneNumber.TryParse(phoneNumberStr, out _).Should().BeTrue(); + } + + [Test] + public void ShouldReturnFalseForInvalid() => + PhoneNumber.TryParse(string.Empty, out _).Should().BeFalse(); + + [Test] + public void ShouldParsePhoneNumberWithCountryCode() + { + const string phoneNumber = "+55(11) 91234-5678"; + const string phoneNumberClean = "+5511912345678"; + + var sut = PhoneNumber.Parse(phoneNumber); + sut.Value.Should().Be(phoneNumberClean); + } + + [Test] + public void ShouldParsePhoneNumber() + { + const string phoneNumber = "(11) 91234-5678"; + const string phoneNumberClean = "11912345678"; + + var sut = PhoneNumber.Parse(phoneNumber); + sut.Value.Should().Be(phoneNumberClean); + } + + + [Test] + public void ShouldComparePhoneNumber() + { + var phone1 = PhoneNumber.Parse("(11) 91234-5678"); + var phone2 = PhoneNumber.Parse("11912345678"); + phone1.Should().Be(phone2); + } + + record Sut(PhoneNumber Value); + + static string Clear(string input) => + Regex.Replace(input, "[^0-9a-zA-Z]+", string.Empty).Trim(); + + [Test] + public void ShouldSerializePhoneNumber() + { + var data = faker.Person.Phone; + var json = JsonSerializer.Serialize(new Sut(PhoneNumber.Parse(data))); + json.Should().Be(@$"{{""Value"":""{Clear(data)}""}}"); + } + + [Test] + public void ShouldDeserializePhoneNumber() + { + var value = PhoneNumber.Parse(faker.Person.Phone); + var body = @$"{{""Value"":""{value.Value}""}}"; + var parsed = JsonSerializer.Deserialize(body)!; + var expected = PhoneNumber.Parse(value); + parsed.Value.Should().Be(expected); + } +}