Code for Roman Numeral Kata post at http://fsharpforfunandprofit.com/posts/roman-numeral-kata/
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
This comment has been minimized.
That's a lot of code for such a small application :)