Skip to content

Instantly share code, notes, and snippets.

@louthy
Last active March 24, 2024 12:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save louthy/09d949f0a62a0061989b64dd4903167f to your computer and use it in GitHub Desktop.
Save louthy/09d949f0a62a0061989b64dd4903167f to your computer and use it in GitHub Desktop.
using System.Numerics;
using LanguageExt;
using LanguageExt.Common;
using LanguageExt.Traits;
using static LanguageExt.Prelude;
namespace ValidationExamples;
// Credit card number
public record CardNumber(Seq<int> Number)
{
public override string ToString() =>
$"{Number}";
}
// Expiry date
public record Expiry(int Month, int Year) :
IAdditionOperators<Expiry, Expiry, Expiry>,
IComparisonOperators<Expiry, Expiry, bool>
{
public static readonly Expiry OneMonth = new (1, 0);
public static Expiry operator +(Expiry left, Expiry right)
{
var m = left.Month + right.Month;
var y = left.Year + right.Year;
while (m > 12) m -= 12;
return new Expiry(m, y);
}
public static bool operator >(Expiry left, Expiry right) =>
left.Year > right.Year ||
left.Year == right.Year && left.Month > right.Month;
public static bool operator >=(Expiry left, Expiry right) =>
left.Year > right.Year ||
left.Year == right.Year && left.Month >= right.Month;
public static bool operator <(Expiry left, Expiry right) =>
left.Year < right.Year ||
left.Year == right.Year && left.Month < right.Month;
public static bool operator <=(Expiry left, Expiry right) =>
left.Year < right.Year ||
left.Year == right.Year && left.Month <= right.Month;
public static Expiry Now
{
get
{
var now = DateTime.Now;
return new Expiry(now.Month, now.Year);
}
}
public static Range<Expiry> NextTenYears =>
LanguageExt.Range.fromMinMax(Now, Now + new Expiry(0, 10), new Expiry(1, 0));
public override string ToString() =>
$"{Month}/{Year}";
}
// CVV code
public record CVV(int Number)
{
public override string ToString() =>
$"{Number}";
}
// Complete credit card details
public record CreditCardDetails(CardNumber CardNumber, Expiry Expiry, CVV CVV)
{
public static CreditCardDetails Make(CardNumber cardNo, Expiry expiry, CVV cvv) =>
new (cardNo, expiry, cvv);
public override string ToString() =>
$"CreditCard({CardNumber}, {Expiry}, {CVV})";
}
public static class CreditCard
{
public static void Test()
{
Console.WriteLine(Validate("4560005094752584", "12-2024", "123"));
Console.WriteLine(Validate("00000", "00-2345", "WXYZ"));
}
public static Validation<Error, CreditCardDetails> Validate(string cardNo, string expiryDate, string cvv) =>
fun<CardNumber, Expiry, CVV, CreditCardDetails>(CreditCardDetails.Make)
.Map(ValidateCardNumber(cardNo))
.Apply(ValidateExpiryDate(expiryDate))
.Apply(ValidateCVV(cvv))
.As();
static Validation<Error, CardNumber> ValidateCardNumber(string cardNo) =>
(ValidateAllDigits(cardNo), ValidateLength(cardNo, 16))
.Apply((digits, _) => digits.ToSeq())
.Bind(ValidateLuhn)
.Map(digits => new CardNumber(digits))
.As()
.MapFail(e => Error.New("card number not valid", e));
static Validation<Error, Expiry> ValidateExpiryDate(string expiryDate) =>
expiryDate.Split(['\\', '/', '-', ' ']) switch
{
[var month, var year] =>
from my in ValidateInt(month) & ValidateInt(year)
let exp = new Expiry(my[0], my[1])
from _ in ValidateInRange(exp, Expiry.NextTenYears )
select exp,
_ => Fail(Error.New($"expected expiry-date in the format: MM/YYYY, but got: {expiryDate}"))
};
static Validation<Error, A> ValidateInRange<A>(A value, Range<A> range)
where A : IAdditionOperators<A, A, A>,
IComparisonOperators<A, A, bool> =>
range.InRange(value)
? Pure(value)
: Fail(Error.New($"expected value in range of {range.From} to {range.To}, but got: {value}"));
static Validation<Error, CVV> ValidateCVV(string cvv) =>
fun<int, string, CVV>((code, _) => new CVV(code))
.Map(ValidateInt(cvv).MapFail(_ => Error.New("CVV code should be a number")))
.Apply(ValidateLength(cvv, 3).MapFail(_ => Error.New("CVV code should be 3 digits in length")))
.As();
static Validation<Error, EnumerableM<int>> ValidateAllDigits(string value) =>
value.AsEnumerableM()
.Traverse(CharToDigit)
.As();
static Validation<Error, int> ValidateInt(string value) =>
ValidateAllDigits(value).Map(_ => int.Parse(value));
static Validation<Error, string> ValidateLength(string value, int length) =>
ValidateLength(value.AsEnumerableM(), length)
.Map(_ => value);
static Validation<Error, K<F, A>> ValidateLength<F, A>(K<F, A> fa, int length)
where F : Foldable<F> =>
fa.Count() == length
? Pure(fa)
: Fail(Error.New($"expected length to be {length}, but got: {fa.Count()}"));
static Validation<Error, int> CharToDigit(char ch) =>
ch is >= '0' and <= '9'
? Pure(ch - '0')
: Fail(Error.New($"expected a digit, but got: {ch}"));
static Validation<Error, Seq<int>> ValidateLuhn(Seq<int> digits)
{
int checkDigit = 0;
for (int i = digits.Length - 2; i >= 0; --i)
{
checkDigit += ((i & 1) is 0) switch
{
true => digits[i] > 4 ? digits[i] * 2 - 9 : digits[i] * 2,
false => digits[i]
};
}
return (10 - checkDigit % 10) % 10 == digits.Last
? Pure(digits)
: Fail(Error.New("invalid card number"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment