Skip to content

Commit

Permalink
Add Phone number model
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasteles committed Jan 24, 2023
1 parent 6cfeda2 commit 5f5b326
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/Cpf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public static bool Validate(in ReadOnlySpan<char> cpfString)


/// <summary>
/// Format Cpnj string.
/// Format Cpf string.
/// If <para name="value" /> has size smaller then expected, this function will pad the value with left 0.
/// </summary>
/// <param name="value">Cpf string representation</param>
Expand Down
2 changes: 1 addition & 1 deletion src/CpfCnpj.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ string DebuggerDisplay() => Value == Empty
/// <returns> true if the validation was successful; otherwise, false.</returns>
public static DocumentType? Validate(in ReadOnlySpan<char> cpfOrCnpj)
{
var cleared = cpfOrCnpj.ClearString();
var cleared = cpfOrCnpj.RemoveNonDigits();
return cleared.Length switch
{
Cnpj.DefaultLength when Cnpj.Validate(cleared) => DocumentType.CNPJ,
Expand Down
4 changes: 2 additions & 2 deletions src/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static string ToBrazilMoneyString(this decimal value, bool moneySuffix =

static class Extensions
{
public static ReadOnlySpan<char> ClearString(in this ReadOnlySpan<char> value) =>
public static ReadOnlySpan<char> RemoveNonDigits(in this ReadOnlySpan<char> value) =>
Regex.Replace(value.ToString(), "[^0-9a-zA-Z]+", string.Empty).AsSpan().Trim();

public static ReadOnlySpan<char> FormatMask(in this ReadOnlySpan<char> value, int size,
Expand All @@ -99,7 +99,7 @@ public static ReadOnlySpan<char> FormatMask(in this ReadOnlySpan<char> value, in
public static ReadOnlySpan<char> FormatClean(in this ReadOnlySpan<char> value, int size) =>
value.IsEmptyOrWhiteSpace()
? string.Empty
: value.ClearString().PadZero(size);
: value.RemoveNonDigits().PadZero(size);

public static ReadOnlySpan<char> PadZero(in this ReadOnlySpan<char> value, int totalWidth)
{
Expand Down
94 changes: 94 additions & 0 deletions src/PhoneNumber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
namespace BrazilModels;

using System;
using System.ComponentModel;
using System.Diagnostics;

/// <summary>
/// Basic strongly typed Phone number representation
/// </summary>
[System.Text.Json.Serialization.JsonConverter(typeof(StringSystemTextJsonConverter<PhoneNumber>))]
[TypeConverter(typeof(StringTypeConverter<PhoneNumber>))]
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
[Swashbuckle.AspNetCore.Annotations.SwaggerSchemaFilter(typeof(StringSchemaFilter))]
public readonly record struct PhoneNumber : IComparable<PhoneNumber>
{
/// <summary>
/// String representation of the Phone number
/// </summary>
public string Value { get; }

/// <summary>
/// Create a new phone number instance
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public PhoneNumber(string phoneNumber)
{
ArgumentNullException.ThrowIfNull(phoneNumber);
this.Value = Format(phoneNumber.ToLowerInvariant());
}

/// <inheritdoc />
public override string ToString() => Value;

/// <summary>
/// Get phoneNumber instance of an Value string
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public static explicit operator PhoneNumber(string value)
=> Parse(value);

/// <summary>
/// Return string representation of phoneNumber
/// </summary>
public static implicit operator string(PhoneNumber phoneNumber)
=> phoneNumber.Value;

/// <summary>
/// Try parse an Value string to an phoneNumber instance
/// </summary>
public static bool TryParse(string? value, out PhoneNumber phoneNumber)
{
phoneNumber = default;
if (string.IsNullOrWhiteSpace(value))
return false;

phoneNumber = new(value);
return true;
}

/// <summary>
/// Parse an Value string to an phoneNumber instance
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public static PhoneNumber Parse(string value) =>
TryParse(value, out var valid)
? valid
: throw new InvalidOperationException($"Invalid phoneNumber {value}");

/// <inheritdoc />
public int CompareTo(PhoneNumber other) =>
string.Compare(Value, other.Value, StringComparison.OrdinalIgnoreCase);

string DebuggerDisplay() => $"PHONE{{{Value}}}";

/// <summary>
/// Format Phone string.
/// </summary>
/// <param name="value">Phone string representation</param>
/// <returns>Formatted Phone string</returns>
public static string Format(in ReadOnlySpan<char> value)
{
if (value.IsEmptyOrWhiteSpace())
return string.Empty;

var clean = value.RemoveNonDigits();
if (value.StartsWith("+"))
return "+" + clean.ToString();

return clean.ToString();
}
}
71 changes: 71 additions & 0 deletions tests/BrazilModels.Tests/PhoneNumberTests.cs
Original file line number Diff line number Diff line change
@@ -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<Sut>(body)!;
var expected = PhoneNumber.Parse(value);
parsed.Value.Should().Be(expected);
}
}

0 comments on commit 5f5b326

Please sign in to comment.