The struct below parses full name into first name, middle name, last name and suffix.
- Perhaps Builder pattern is more appropriate here? I remember there was a principle referring to the number of constructor parameters which Builder solves.
- record vs struct? Records inherit from
IEquatable
. https://sharplab.io/#v2:CYLg1APgAgzABAJwKYGMD2DhwB4FgBQA3gXHAJYB2ALnAIYDcBAvgUA= - Is it a good idea to replace
Parse
withTryParse
pattern?
Code
/// <summary>
/// Represents a person's name.
/// </summary>
public struct FullName : IEquatable<FullName>, IFormattable
{
/// <summary>
/// Initializes a new instance of the <see cref="FullName" /> struct.
/// </summary>
/// <param name="firstName">The given name.</param>
/// <param name="lastName">The family name.</param>
/// <param name="middleName">The middle name.</param>
/// <param name="suffix">The suffix.</param>
public FullName(string? firstName, string? lastName = default, string? middleName = default,
string? suffix = default)
{
FirstName = firstName;
MiddleName = middleName;
LastName = lastName;
Suffix = suffix;
}
/// <summary>
/// Gets or sets the person's first name.
/// </summary>
public string? FirstName { get; set; }
/// <summary>
/// Gets or sets the middle name.
/// </summary>
public string? MiddleName { get; set; }
/// <summary>
/// Gets or sets the person's last name.
/// </summary>
public string? LastName { get; set; }
/// <summary>
/// Gets or sets the suffix. (e.g. PhD, Jr, etc)
/// </summary>
public string? Suffix { get; set; }
/// <summary>
/// Parses the specified text.
/// </summary>
/// <param name="text">The text.</param>
public static FullName Parse(string text)
{
if (string.IsNullOrEmpty(text))
{
return new FullName();
}
var parts = text.Split(' ');
string? Get(int idx)
{
return idx >= parts.Length ? null : parts[idx];
}
switch (parts.Length)
{
case 1:
case 2:
return new FullName(Get(0), Get(1));
case 3:
var last = Get(2);
return IsSuffix(last)
? new FullName(Get(0), Get(1), default, last)
: new FullName(Get(0), Get(2), Get(1));
case 4:
return new FullName(Get(0), Get(2), Get(1), Get(3));
}
return new FullName();
}
#region IFormattable
/// <summary>Converts to string.</summary>
/// <returns>A <see cref="System.String" /> that represents this instance.</returns>
public override string ToString()
{
return string.Join(" ", FirstName, MiddleName, LastName, Suffix).Replace(" ", " ").Trim();
}
/// <summary>
/// Converts to string.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public string ToString(string format)
{
return ToString(string.Concat("{0:", string.IsNullOrEmpty(format) ? "G" : format, '}'), Formatter);
}
/// <summary>
/// Converts to string.
/// </summary>
/// <param name="format">The format.</param>
/// <param name="formatProvider">The format provider.</param>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public string ToString(string? format, IFormatProvider? formatProvider)
{
return string.Format(formatProvider ?? Formatter, string.IsNullOrEmpty(format) ? "{0:G}" : format, this);
}
public static implicit operator string(FullName obj)
{
return obj.ToString();
}
public static explicit operator FullName(string obj)
{
return Parse(obj);
}
#endregion IFormattable
#region IEquatable
/// <summary>
/// Determines if the specified names are equal.
/// </summary>
/// <param name="a">The first name.</param>
/// <param name="b">The other name.</param>
/// <param name="comparison">The comparison (defaults to <see cref="StringComparison.InvariantCultureIgnoreCase" />).</param>
public static bool Equals(FullName a, FullName b,
StringComparison comparison = StringComparison.InvariantCultureIgnoreCase)
{
return
string.Equals(a.FirstName, b.FirstName, comparison) && string.Equals(a.MiddleName, b.MiddleName, comparison)
&& string.Equals(a.LastName, b.LastName, comparison)
&& string.Equals(a.Suffix, b.Suffix, comparison);
}
public static bool operator ==(FullName x, FullName y)
{
return Equals(x, y);
}
public static bool operator !=(FullName x, FullName y)
{
return !Equals(x, y);
}
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>
/// true if the current object is equal to the <paramref name="other">other</paramref> parameter; otherwise, false.
/// </returns>
public bool Equals(FullName other)
{
return Equals(this, other);
}
/// <summary>
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
/// <returns>
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
public override bool Equals(object? obj)
{
return obj is FullName name && Equals(this, name);
}
public override int GetHashCode()
{
return
FirstName?.GetHashCode() ?? 0 ^
MiddleName?.GetHashCode() ?? 0 ^
LastName?.GetHashCode() ?? 0 ^
Suffix?.GetHashCode() ?? 0;
}
#endregion IEquatable
#region Backing Members
internal static bool IsSuffix(string text)
{
// NOTE: A suffix is a string that ends with a period (.)
// or has a uppercase beyond the first character.
if (string.IsNullOrEmpty(text))
{
return false;
}
if (text[^1] == '.')
{
return true;
}
for (var i = 1; i < text.Length; i++)
{
var c = text[i];
if (char.IsLetter(c) && char.IsUpper(c))
{
return true;
}
}
return text.Length is 3 or 4 || IsAllCaps(text);
}
internal static bool IsAllCaps(string text)
{
return text.All(c => !char.IsLetter(c) || !char.IsLower(c));
}
private static NameFormatter? _formatter;
private static NameFormatter Formatter => _formatter ??= new NameFormatter();
#endregion Backing Members
}
internal class NameFormatter : ICustomFormatter, IFormatProvider
{
public string Format(string? format, object? arg, IFormatProvider? formatProvider)
{
if (arg is FullName name)
{
var n = format.Length;
var builder = new StringBuilder();
for (var i = 0; i < n; i++)
{
char c;
switch (c = format[i])
{
default:
builder.Append(c);
break;
case 'G':
builder.Append(name.ToString());
break;
case 'S':
builder.Append(string.Join(" ", name.FirstName, name.LastName).Trim());
break;
case '1':
case 'f':
case 'F':
builder.Append(name.FirstName);
break;
case '2':
case 'm':
case 'M':
builder.Append(name.MiddleName);
break;
case '3':
case 'l':
case 'L':
builder.Append(name.LastName);
break;
case '\\': /* Escape */
builder.Append(i + 1 < n ? format[++i] : c);
break;
}
}
return builder.ToString().Trim();
}
return GetFallbackFormat(format, arg);
}
public object? GetFormat(Type? formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
private string? GetFallbackFormat(string format, object? arg)
{
if (arg is IFormattable formattable)
{
return formattable.ToString(format, CultureInfo.CurrentCulture);
}
return arg != null ? arg.ToString() : string.Empty;
}
}
Unit tests
public sealed class FullNameTests
{
[Theory]
[InlineData("Иван Иванов", "Иван", default, "Иванов")]
[InlineData("Анна-Мария Драганова Иванова", "Анна-Мария", "Драганова", "Иванова")]
public void Build_ShouldReturnCorrectUsernamePassword_WhenGivenFullName(
string fullName,
string expectedFirstName,
string expectedMiddleName,
string expectedLastName)
{
// Arrange
// Act
var actual = FullName.Parse(fullName);
// Assert
actual.FirstName.Should().Be(expectedFirstName);
actual.MiddleName.Should().Be(expectedMiddleName);
actual.LastName.Should().Be(expectedLastName);
}
}