Last active
January 22, 2021 23:57
-
-
Save swlaschin/8409306 to your computer and use it in GitHub Desktop.
Code for Roman Numeral Kata post at http://fsharpforfunandprofit.com/posts/roman-numeral-kata/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
System.IO.Directory.SetCurrentDirectory __SOURCE_DIRECTORY__ | |
// The following DLLs were fetched using NuGet | |
#r @".\packages\FsCheck.0.9.2.0\lib\net40-Client\FsCheck.dll" | |
#r @".\packages\NUnit.2.6.3\lib\nunit.framework.dll" | |
open FsCheck | |
open NUnit.Framework | |
// ====================================== | |
// Port of the ruby test code | |
// ====================================== | |
module PortOfRubyCode = | |
let arabicToRoman arabic = | |
"" // implementation to do | |
[<Test>] | |
let ``For certain inputs, expect certain outputs``() = | |
let testpairs = [ | |
(1,"I") | |
(2,"II") | |
(4,"IV") | |
(5,"V") | |
(9,"IX") | |
(10,"X") | |
// etc | |
(900,"CM") | |
(1000,"M") | |
(3497,"MMMCDXCVII") | |
] | |
for (arabic,expectedRoman) in testpairs do | |
let roman = arabicToRoman arabic | |
Assert.AreEqual(expectedRoman, roman) | |
// ====================================== | |
// Testing all inputs | |
// ====================================== | |
module TestingAllInputs = | |
let arabicToRoman arabic = | |
"" // implementation to do | |
let assertMaxRepetition str count input = | |
() // implementation to do | |
[<Test>] | |
let ``For all valid inputs, there must be a max of four "I"s in a row``() = | |
for i in [1..4000] do | |
let roman = arabicToRoman i | |
roman |> assertMaxRepetition "I" 4 | |
let assertSuffix suffix input = | |
() // implementation to do | |
let assertNonSuffix suffix input = | |
() // implementation to do | |
[<Test>] | |
let ``For all valid inputs with two in the units, there must be exactly two "I"s at the end``() = | |
for i in [2..10..4000] do // 2 to 4000 by steps of 10 | |
let roman = arabicToRoman i | |
roman |> assertSuffix "II" | |
roman |> assertNonSuffix "III" | |
// ====================================== | |
// Property based testing example | |
// ====================================== | |
module PropertyBasedTestingExample = | |
let arabicToRoman arabic = | |
"" // implementation to do | |
let assertMaxRepetition str count input = | |
() // implementation to do | |
// Define a property that should be true for all inputs | |
let ``has max rep of four Is`` arabic = | |
let roman = arabicToRoman arabic | |
roman |> assertMaxRepetition "I" 4 | |
// Explicitly enumerate all inputs... | |
[<Test>] | |
let ``For all valid inputs, there must be a max of four "I"s``() = | |
for i in [1..4000] do | |
//check that the property holds | |
``has max rep of four Is`` i | |
// ...Or use FsCheck to generate inputs for you | |
do | |
let isInRange i = (i >= 1) && (i <= 4000) | |
// input is in range implies has max of four Is | |
let prop i = isInRange i ==> ``has max rep of four Is`` i | |
// check all inputs for this property | |
Check.Quick prop | |
let assertMaxOccurs str max input = | |
() // implementation to do | |
// Define a property that should be true for all inputs | |
let ``if arabic has 4 tens then roman has one XL otherwise none`` arabic = | |
let roman = arabicToRoman arabic | |
let has4Tens = (arabic % 100 / 10) = 4 | |
if has4Tens then | |
assertMaxOccurs "XL" 1 roman | |
else | |
assertMaxOccurs "XL" 0 roman | |
// Explicitly enumerate all inputs... | |
[<Test>] | |
let ``For all valid inputs, check the XL substitution``() = | |
for i in [1..4000] do | |
``if arabic has 4 tens then roman has one XL otherwise none`` i | |
// ...Or again use FsCheck to generate inputs for you | |
do | |
let isInRange i = (i >= 1) && (i <= 4000) | |
let prop i = isInRange i ==> ``if arabic has 4 tens then roman has one XL otherwise none`` i | |
Check.Quick prop | |
// ====================================== | |
// Complete property based testing using CustomAssert | |
// ====================================== | |
module Test_Using_CustomAssert = | |
// the implementation to use is set later | |
let mutable arabicToRoman' : int -> string = | |
fun arabic -> failwith "Must be set for testing" | |
// ====================================================== | |
// helper functions | |
// ====================================================== | |
let inputRange() = [1..4000] | |
let assertTrue(b,msg) = | |
if not b then printfn "FAIL: %s" msg | |
let assertMaxRepetition str count (input:string) = | |
let find = String.replicate (count+1) str | |
let b = input.Contains find |> not | |
let msg = sprintf "assertMaxRepetition %s %i %s" str count input | |
assertTrue(b,msg) | |
let assertOccurs (str:string) count (input:string) = | |
// if it occurs once, then the string will be split into two, etc. | |
let b = input.Split( [| str |],System.StringSplitOptions.None).Length = count + 1 | |
let msg = sprintf "assertOccurs %s %i %s" str count input | |
assertTrue(b,msg) | |
let replace (oldVal,newVal) (s:string) = | |
s.Replace(oldValue=oldVal,newValue=newVal) | |
let removeAbbreviations (s:string) = | |
s | |
|> replace("IV","IIII") | |
|> replace("IX","VIIII") | |
|> replace("XL","XXXX") | |
|> replace("XC","LXXXX") | |
|> replace("CD","CCCC") | |
|> replace("CM","DCCCC") | |
let assertSortedHighToLow (s:string) = | |
let unsorted = | |
s | |
|> removeAbbreviations | |
|> replace("I","1") | |
|> replace("V","2") | |
|> replace("X","3") | |
|> replace("L","4") | |
|> replace("C","5") | |
|> replace("D","6") | |
|> replace("M","7") | |
|> List.ofSeq | |
let sortedHighToLow = unsorted |> List.sort |> List.rev | |
//assert they are the same | |
let b = unsorted = sortedHighToLow | |
let msg = sprintf "assertSortedHighToLow %s" s | |
assertTrue(b,msg) | |
// ====================================================== | |
// Properties | |
// ====================================================== | |
let maxRepetitionProperty str count = | |
inputRange() |> List.iter (fun arabic -> | |
let roman = arabicToRoman' arabic | |
roman |> assertMaxRepetition str count | |
) | |
printfn "Completed maxRepetitionProperty %s %i" str count | |
let foursProperty place str = | |
inputRange() |> List.iter (fun arabic -> | |
let roman = arabicToRoman' arabic | |
let has4s = (arabic % (place*10) / place) = 4 | |
if has4s then | |
assertOccurs str 1 roman | |
else | |
assertOccurs str 0 roman | |
) | |
printfn "Completed foursProperty %i %s" place str | |
let ninesProperty place str = | |
inputRange() |> List.iter (fun arabic -> | |
let roman = arabicToRoman' arabic | |
let has9s = (arabic % (place*10) / place) = 9 | |
if has9s then | |
assertOccurs str 1 roman | |
else | |
assertOccurs str 0 roman | |
) | |
printfn "Completed ninesProperty %i %s" place str | |
let ``has max rep of three Is``() = | |
maxRepetitionProperty "I" 3 | |
let ``has max rep of one V``() = | |
maxRepetitionProperty "V" 1 | |
let ``has max rep of three Xs``() = | |
maxRepetitionProperty "X" 3 | |
let ``has max rep of one L``() = | |
maxRepetitionProperty "L" 1 | |
let ``has max rep of three Cs``() = | |
maxRepetitionProperty "C" 3 | |
let ``has max rep of one D``() = | |
maxRepetitionProperty "D" 1 | |
let ``has max rep of four Ms``() = | |
maxRepetitionProperty "M" 4 | |
let ``if arabic has 4 ones then roman has one IV otherwise none``() = | |
foursProperty 1 "IV" | |
let ``if arabic has 9 ones then roman has one IX otherwise none``() = | |
ninesProperty 1 "IX" | |
let ``if arabic has 4 tens then roman has one XL otherwise none``() = | |
foursProperty 10 "XL" | |
let ``if arabic has 9 tens then roman has one XC otherwise none``() = | |
ninesProperty 10 "XC" | |
let ``if arabic has 4 hundreds then roman has one CD otherwise none``() = | |
foursProperty 100 "CD" | |
let ``if arabic has 9 hundreds then roman has one CM otherwise none``() = | |
ninesProperty 100 "CM" | |
let ``is sorted from M to I``() = | |
inputRange() |> List.iter (fun arabic -> | |
let roman = arabicToRoman' arabic | |
roman |> assertSortedHighToLow | |
) | |
printfn "Completed is sorted from M to I" | |
let checkAllProperties() = | |
``has max rep of three Is``() | |
``has max rep of one V``() | |
``has max rep of three Xs``() | |
``has max rep of one L``() | |
``has max rep of three Cs``() | |
``has max rep of one D``() | |
``has max rep of four Ms``() | |
``if arabic has 4 ones then roman has one IV otherwise none``() | |
``if arabic has 9 ones then roman has one IX otherwise none``() | |
``if arabic has 4 tens then roman has one XL otherwise none``() | |
``if arabic has 9 tens then roman has one XC otherwise none``() | |
``if arabic has 4 hundreds then roman has one CD otherwise none``() | |
``if arabic has 9 hundreds then roman has one CM otherwise none``() | |
``is sorted from M to I``() | |
// ====================================== | |
// Complete property based testing using FsCheck | |
// ====================================== | |
module Test_Using_FsCheck = | |
// the implementation to use is set later | |
let mutable arabicToRoman' : int -> string = | |
fun arabic -> failwith "Must be set for testing" | |
// ====================================================== | |
// helper functions | |
// ====================================================== | |
let inputRange = Gen.choose(1,4000) |> Arb.fromGen | |
let assertMaxRepetition str count (input:string) = | |
let find = String.replicate (count+1) str | |
input.Contains find |> not | |
let assertOccurs (str:string) count (input:string) = | |
// if it occurs once, then the string will be split into two, etc. | |
input.Split( [| str |],System.StringSplitOptions.None).Length = count + 1 | |
let replace (oldVal,newVal) (s:string) = | |
s.Replace(oldValue=oldVal,newValue=newVal) | |
let removeAbbreviations (s:string) = | |
s | |
|> replace("IV","IIII") | |
|> replace("IX","VIIII") | |
|> replace("XL","XXXX") | |
|> replace("XC","LXXXX") | |
|> replace("CD","CCCC") | |
|> replace("CM","DCCCC") | |
let assertSortedHighToLow (s:string) = | |
let unsorted = | |
s | |
|> removeAbbreviations | |
|> replace("I","1") | |
|> replace("V","2") | |
|> replace("X","3") | |
|> replace("L","4") | |
|> replace("C","5") | |
|> replace("D","6") | |
|> replace("M","7") | |
|> List.ofSeq | |
let sortedHighToLow = unsorted |> List.sort |> List.rev | |
//assert they are the same | |
unsorted = sortedHighToLow | |
// ====================================================== | |
// Properties | |
// ====================================================== | |
type ImplementationProperties() = | |
static let maxRepetitionProperty str count = | |
Prop.forAll inputRange (fun arabic -> | |
let roman = arabicToRoman' arabic | |
roman |> assertMaxRepetition str count | |
) | |
static let foursProperty place str = | |
Prop.forAll inputRange (fun arabic -> | |
let roman = arabicToRoman' arabic | |
let has4s = (arabic % (place*10) / place) = 4 | |
if has4s then | |
assertOccurs str 1 roman | |
else | |
assertOccurs str 0 roman | |
) | |
static let ninesProperty place str = | |
Prop.forAll inputRange (fun arabic -> | |
let roman = arabicToRoman' arabic | |
let has9s = (arabic % (place*10) / place) = 9 | |
if has9s then | |
assertOccurs str 1 roman | |
else | |
assertOccurs str 0 roman | |
) | |
static member ``has max rep of three Is`` = | |
maxRepetitionProperty "I" 3 | |
static member ``has max rep of one V`` = | |
maxRepetitionProperty "V" 1 | |
static member ``has max rep of three Xs`` = | |
maxRepetitionProperty "X" 3 | |
static member ``has max rep of one L`` = | |
maxRepetitionProperty "L" 1 | |
static member ``has max rep of three Cs`` = | |
maxRepetitionProperty "C" 3 | |
static member ``has max rep of one D`` = | |
maxRepetitionProperty "D" 1 | |
static member ``has max rep of four Ms`` = | |
maxRepetitionProperty "M" 4 | |
static member ``if arabic has 4 ones then roman has one IV otherwise none`` = | |
foursProperty 1 "IV" | |
static member ``if arabic has 9 ones then roman has one IX otherwise none`` = | |
ninesProperty 1 "IX" | |
static member ``if arabic has 4 tens then roman has one XL otherwise none`` = | |
foursProperty 10 "XL" | |
static member ``if arabic has 9 tens then roman has one XC otherwise none`` = | |
ninesProperty 10 "XC" | |
static member ``if arabic has 4 hundreds then roman has one CD otherwise none`` = | |
foursProperty 100 "CD" | |
static member ``if arabic has 9 hundreds then roman has one CM otherwise none`` = | |
ninesProperty 100 "CM" | |
static member ``is sorted from M to I`` = | |
Prop.forAll inputRange (fun arabic -> | |
let roman = arabicToRoman' arabic | |
roman |> assertSortedHighToLow | |
) | |
// ====================================== | |
// Tally-based implementation -- version 1 | |
// ====================================== | |
module TallyImplementation_V1 = | |
let arabicToRoman arabic = | |
String.replicate arabic "I" | |
module TallyImplementation_V1_Demo = | |
open TallyImplementation_V1 | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IIII" | |
testArabicToRoman 5 // "IIIII" | |
testArabicToRoman 6 // "IIIIII" | |
testArabicToRoman 10 // "IIIIIIIIII" | |
testArabicToRoman 12 // "IIIIIIIIIIII" | |
testArabicToRoman 16 // "IIIIIIIIIIIIIIII" | |
testArabicToRoman 40 // "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII" | |
// ====================================== | |
// Tally-based implementation -- version 2 | |
// ====================================== | |
module TallyImplementation_V2 = | |
let arabicToRoman arabic = | |
(String.replicate arabic "I") | |
.Replace("IIIII","V") | |
module TallyImplementation_V2_Demo = | |
open TallyImplementation_V2 | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IIII" | |
testArabicToRoman 5 // "V" | |
testArabicToRoman 6 // "VI" | |
testArabicToRoman 10 // "VV" | |
testArabicToRoman 12 // "VVII" | |
testArabicToRoman 16 // "VVVI" | |
testArabicToRoman 40 // "VVVVVVVV" | |
// ====================================== | |
// Tally-based implementation -- version 3 | |
// ====================================== | |
module TallyImplementation_V3 = | |
let arabicToRoman arabic = | |
(String.replicate arabic "I") | |
.Replace("IIIII","V") | |
.Replace("VV","X") | |
module TallyImplementation_V3_Demo = | |
open TallyImplementation_V3 | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IIII" | |
testArabicToRoman 5 // "V" | |
testArabicToRoman 6 // "VI" | |
testArabicToRoman 10 // "X" | |
testArabicToRoman 12 // "XII" | |
testArabicToRoman 16 // "XVI" | |
testArabicToRoman 40 // "XXXX" | |
// ====================================== | |
// Tally-based implementation -- V4 | |
// ====================================== | |
module TallyImplementation_V4 = | |
let arabicToRoman arabic = | |
(String.replicate arabic "I") | |
.Replace("IIIII","V") | |
.Replace("VV","X") | |
.Replace("XXXXX","L") | |
.Replace("LL","C") | |
.Replace("CCCCC","D") | |
.Replace("DD","M") | |
// optional substitutions | |
.Replace("IIII","IV") | |
.Replace("VIV","IX") | |
.Replace("XXXX","XL") | |
.Replace("LXL","XC") | |
.Replace("CCCC","CD") | |
.Replace("DCD","CM") | |
module TallyImplementation_V4_Demo = | |
open TallyImplementation_V4 | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IV" | |
testArabicToRoman 5 // "V" | |
testArabicToRoman 6 // "VI" | |
testArabicToRoman 10 // "X" | |
testArabicToRoman 12 // "XII" | |
testArabicToRoman 16 // "XVI" | |
testArabicToRoman 40 // "XL" | |
testArabicToRoman 946 // "CMXLVI" | |
testArabicToRoman 3497 // "MMMCDXCVII" | |
// ====================================== | |
// Tally-based implementation -- complete with property based tests | |
// ====================================== | |
module TallyImplementation_Complete = | |
let arabicToRoman' arabic = | |
(String.replicate arabic "I") | |
.Replace("IIIII","V") | |
.Replace("VV","X") | |
.Replace("XXXXX","L") | |
.Replace("LL","C") | |
.Replace("CCCCC","D") | |
.Replace("DD","M") | |
// optional substitutions | |
.Replace("IIII","IV") | |
.Replace("VIV","IX") | |
.Replace("XXXX","XL") | |
.Replace("LXL","XC") | |
.Replace("CCCC","CD") | |
.Replace("DCD","CM") | |
let arabicToRoman arabic = | |
if arabic < 0 then None | |
else if arabic > 4000 then None | |
else if arabic = 0 then Some "" | |
else Some (arabicToRoman' arabic) | |
module TallyImplementation_Complete_Demo = | |
open TallyImplementation_Complete | |
let testArabicToRoman i = | |
printfn "%i ==> %A" i (arabicToRoman i) | |
testArabicToRoman -1 // None | |
testArabicToRoman 0 // Some "" | |
testArabicToRoman 1 // Some "I" | |
testArabicToRoman 4 // Some "IV" | |
testArabicToRoman 5 // Some "V" | |
testArabicToRoman 6 // Some "VI" | |
testArabicToRoman 10 // Some "X" | |
testArabicToRoman 12 // Some "XII" | |
testArabicToRoman 16 // Some "XVI" | |
testArabicToRoman 40 // Some "XL" | |
testArabicToRoman 946 // Some "CMXLVI" | |
testArabicToRoman 3497 // Some "MMMCDXCVII" | |
testArabicToRoman 10000 // None | |
module TallyImplementation_Complete_Test_Using_CustomAssert = | |
// set the implementation to use | |
Test_Using_CustomAssert.arabicToRoman' <- TallyImplementation_Complete.arabicToRoman' | |
// check all the properties | |
Test_Using_CustomAssert.checkAllProperties() | |
module TallyImplementation_Complete_Test_Using_FsCheck = | |
// set the implementation to use | |
Test_Using_FsCheck.arabicToRoman' <- TallyImplementation_Complete.arabicToRoman' | |
// check all the properties | |
Check.All<Test_Using_FsCheck.ImplementationProperties>({Config.Quick with MaxTest = 4000}) | |
// ====================================== | |
// place-based implementation without abbreviations | |
// ====================================== | |
module PlaceBasedImplementation_WithoutAbbreviation = | |
let biQuinaryDigits place (unit,five) arabic = | |
let digit = arabic % (10*place) / place | |
match digit with | |
| 0 -> "" | |
| 1 -> unit | |
| 2 -> unit + unit | |
| 3 -> unit + unit + unit | |
| 4 -> unit + unit + unit + unit | |
| 5 -> five | |
| 6 -> five + unit | |
| 7 -> five + unit + unit | |
| 8 -> five + unit + unit + unit | |
| 9 -> five + unit + unit + unit + unit | |
| _ -> failwith "Expected 0-9 only" | |
let arabicToRoman' arabic = | |
let units = biQuinaryDigits 1 ("I","V") arabic | |
let tens = biQuinaryDigits 10 ("X","L") arabic | |
let hundreds = biQuinaryDigits 100 ("C","D") arabic | |
let thousands = biQuinaryDigits 1000 ("M","?") arabic | |
thousands + hundreds + tens + units | |
let arabicToRoman arabic = | |
if arabic < 0 then None | |
else if arabic > 4000 then None | |
else if arabic = 0 then Some "" | |
else Some (arabicToRoman' arabic) | |
module PlaceBasedImplementation_WithoutAbbreviation_Demo = | |
open PlaceBasedImplementation_WithoutAbbreviation | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman' i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IIII" | |
testArabicToRoman 5 // "V" | |
testArabicToRoman 6 // "VI" | |
testArabicToRoman 10 // "X" | |
testArabicToRoman 12 // "XII" | |
testArabicToRoman 16 // "XVI" | |
testArabicToRoman 40 // "XXXX" | |
testArabicToRoman 946 // "DCCCCXXXXVI" | |
testArabicToRoman 3497 // "MMMCCCCLXXXXVII" | |
// ====================================== | |
// place-based implementation | |
// ====================================== | |
module PlaceBasedImplementation = | |
let biQuinaryDigits place (unit,five,ten) arabic = | |
let digit = arabic % (10*place) / place | |
match digit with | |
| 0 -> "" | |
| 1 -> unit | |
| 2 -> unit + unit | |
| 3 -> unit + unit + unit | |
| 4 -> unit + five // changed to be one less than five | |
| 5 -> five | |
| 6 -> five + unit | |
| 7 -> five + unit + unit | |
| 8 -> five + unit + unit + unit | |
| 9 -> unit + ten // changed to be one less than ten | |
| _ -> failwith "Expected 0-9 only" | |
let arabicToRoman' arabic = | |
let units = biQuinaryDigits 1 ("I","V","X") arabic | |
let tens = biQuinaryDigits 10 ("X","L","C") arabic | |
let hundreds = biQuinaryDigits 100 ("C","D","M") arabic | |
let thousands = biQuinaryDigits 1000 ("M","?","?") arabic | |
thousands + hundreds + tens + units | |
let arabicToRoman arabic = | |
if arabic < 0 then None | |
else if arabic > 4000 then None | |
else if arabic = 0 then Some "" | |
else Some (arabicToRoman' arabic) | |
module PlaceBasedImplementation_Demo = | |
open PlaceBasedImplementation | |
let testArabicToRoman i = | |
printfn "%i ==> %s" i (arabicToRoman' i) | |
testArabicToRoman 1 // "I" | |
testArabicToRoman 4 // "IV" | |
testArabicToRoman 5 // "V" | |
testArabicToRoman 6 // "VI" | |
testArabicToRoman 10 // "X" | |
testArabicToRoman 12 // "XII" | |
testArabicToRoman 16 // "XVI" | |
testArabicToRoman 40 // "XL" | |
testArabicToRoman 946 // "CMXLVI" | |
testArabicToRoman 3497 // "MMMCDXCVII" | |
testArabicToRoman 4000 // "M?" | |
module PlaceBasedImplementation_Test_Using_CustomAssert = | |
// set the implementation to use | |
Test_Using_CustomAssert.arabicToRoman' <- PlaceBasedImplementation.arabicToRoman' | |
// check all the properties | |
Test_Using_CustomAssert.checkAllProperties() | |
// Note that some of the tests fail when the input is 4000. | |
// This is because the output is "M?" rather than "MMMM" | |
module PlaceBasedImplementation_Test_Using_FsCheck = | |
// set the implementation to use | |
Test_Using_FsCheck.arabicToRoman' <- PlaceBasedImplementation.arabicToRoman' | |
// check all the properties | |
Check.All<Test_Using_FsCheck.ImplementationProperties>({Config.Quick with MaxTest = 4000}) | |
// Note that some of the tests fail when the input is 4000. | |
// This is because the output is "M?" rather than "MMMM" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
That's a lot of code for such a small application :)