The most valuable of all talents is that of never using two words when one will do. Thomas Jefferson.
I found it useful to automatically normalize string content a little bit and explicitly state what kind of content could be held in a field:
[TestMethod]
public void Normalize()
{
var name = new Name(" Thomas ", null, " Jefferson \n \r ");
Assert.AreEqual("Thomas", name.First);
Assert.AreEqual("", name.Middle);
Assert.AreEqual("Jefferson", name.Last);
}
where test uses the following demo class:
class Name
{
public Name(string first, string middle, string last)
: this((Word)first, (WordOrEmpty)middle, (Word)last)
{
}
public Name(Word first, WordOrEmpty middle, Word last)
{
First = first;
Middle = middle;
Last = last;
}
public Word First { get; }
public WordOrEmpty Middle { get; }
public Word Last { get; }
}
Library classes are:
public class Text : String<Text>, IEnumerable<Line>
{
public static explicit operator Text(string text) => new Text(text);
public Text(string text)
: base(text, EmptyIfNull, Trim)
{
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<Line> GetEnumerator() => Text
.Split(new[] { "\r\n", "\n\r", "\r", "\n" }, StringSplitOptions.None)
.Select(l => (Line)l)
.GetEnumerator();
}
And:
public class Line : String<Line>, IEnumerable<Word>
{
public static explicit operator Line(string text) => new Line(text);
public Line(string text)
: base(text, EmptyIfNull, Trim, SpaceIfNewLine)
{
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<Word> GetEnumerator() => Text
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(l => (Word)l)
.GetEnumerator();
}
And:
public class LineOrNull : String<LineOrNull>, IEnumerable<Word>
{
public static explicit operator LineOrNull(string text) => new LineOrNull(text);
public LineOrNull(string text)
: base(text, NullIfEmpty, Trim, SpaceIfNewLine)
{
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<Word> GetEnumerator() => (Text ?? "")
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(l => (Word)l)
.GetEnumerator();
}
And:
public class Word : String<Word>
{
public static explicit operator Word(string text) => new Word(text);
public Word(string text)
: base(text, NotNullOrWhitespace, Trim, NotMultiline, NoSpace)
{
}
}
And:
public class WordOrNull : String<WordOrNull>
{
public static explicit operator WordOrNull(string text) => new WordOrNull(text);
public WordOrNull(string text)
: base(text, Trim, NullIfEmpty, NotMultiline, NoSpace)
{
}
}
And:
public class WordOrEmpty : String<WordOrEmpty>
{
public static explicit operator WordOrEmpty(string text) => new WordOrEmpty(text);
public WordOrEmpty(string text)
: base(text, Trim, EmptyIfNull, NotMultiline, NoSpace)
{
}
}
Where:
[JsonConverter(typeof(StringJsonConverter))]
public abstract class String<T> : ValueObject<T>
where T: String<T>
{
protected static string Trim(string text) => text?.Trim();
protected static string EmptyIfNull(string text) => text ?? Empty;
protected static string NullIfEmpty(string text) => IsNullOrWhiteSpace(text) ? null : text;
protected static string SpaceIfNewLine(string text) => text
?.Replace("\n\r", " ")
?.Replace("\r\n", " ")
?.Replace("\r", " ")
?.Replace("\n", " ");
protected static string Upper(string text) => text?.ToUpper();
protected static string Lower(string text) => text?.ToLower();
protected static string NotNull(string text) =>
text ?? throw new TextException();
protected static string NotNullOrWhitespace(string text) =>
IsNullOrWhiteSpace(text) ? throw new TextException() :
text;
protected static string NotNullOrEmpty(string text) =>
IsNullOrEmpty(text) ? throw new TextException() :
text;
protected static string NoSpace(string text) =>
text == null ? null :
text.Contains(' ') ? throw new TextException() :
text;
protected static string NotMultiline(string text) =>
text == null ? null :
text.Contains('\n') || text.Contains('\r') ? throw new TextException() :
text;
public static implicit operator string(String<T> s) => s?.Text;
protected String(string text, params Func<string, string>[] actions) =>
Text = actions.Aggregate(text, (acc, f) => f(acc));
public string Text { get; set; }
public override string ToString() => Text;
protected override IEnumerable<object> EqualityCheckAttributes =>
new[] { Text };
}
Where:
public class TextException : Exception
{
public TextException([CallerMemberName] string rule = null)
: base($"Must be {rule}.")
{
}
}
And:
class StringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>
objectType == typeof(object) ? false :
objectType.IsConstructedGenericType && objectType.GetGenericTypeDefinition() == typeof(String<>) ? true :
CanConvert(objectType.BaseType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
Activator.CreateInstance(objectType, reader.Value);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
writer.WriteValue(value.ToString());
}
And:
public abstract class ValueObject<T> : IEquatable<ValueObject<T>>
where T : ValueObject<T>
{
protected abstract IEnumerable<object> EqualityCheckAttributes { get; }
public override int GetHashCode() =>
EqualityCheckAttributes
.Aggregate(0, (hash, a) => unchecked(hash * 31 + (a?.GetHashCode() ?? 0)));
public override bool Equals(object obj) =>
Equals(obj as ValueObject<T>);
public virtual bool Equals(ValueObject<T> other) =>
other != null &&
GetType() == other.GetType() &&
EqualityCheckAttributes.SequenceEqual(other.EqualityCheckAttributes);
public static bool operator ==(ValueObject<T> left, ValueObject<T> right) =>
Equals(left, right);
public static bool operator !=(ValueObject<T> left, ValueObject<T> right) =>
!Equals(left, right);
}
Name
? It's not clear to me why it should be the job ofName
(or other similar classes) to normalise those inputs. \$\endgroup\$StreamWriter
ctor overloads. It saves a bunch of typing by having a primitive type based overload, especially while unit testing. \$\endgroup\$static
helpers in nested classes likeFilters
andConstraints
so their usage would be easier to understand like:base(text, Filters.Trim, Filters.NullIfEmpty, Constraints.NotMultiline, Constraints.NoSpace)
\$\endgroup\$Use.Trimmed
,Use.NullIfEmpty
andOnly.NotMultiline
sound? \$\endgroup\$