diff --git a/Numsense.UnitTests.CSharp/NumeralConverterProperties.cs b/Numsense.UnitTests.CSharp/NumeralConverterProperties.cs index 015d9c1..8eef203 100644 --- a/Numsense.UnitTests.CSharp/NumeralConverterProperties.cs +++ b/Numsense.UnitTests.CSharp/NumeralConverterProperties.cs @@ -111,7 +111,11 @@ public static Arbitrary Converter() new ConverterPropertyGroup( new GermanNumeralConverter(), NumeralModule.toGerman, - NumeralModule.tryParseGerman) + NumeralModule.tryParseGerman), + new ConverterPropertyGroup( + new BrazilianNumeralConverter(), + NumeralModule.toBrazilian, + NumeralModule.tryParseBrazilian) ) .ToArbitrary(); } diff --git a/Numsense.UnitTests.CSharp/NumeralTests.cs b/Numsense.UnitTests.CSharp/NumeralTests.cs index b8cb841..7d3acd7 100644 --- a/Numsense.UnitTests.CSharp/NumeralTests.cs +++ b/Numsense.UnitTests.CSharp/NumeralTests.cs @@ -199,5 +199,20 @@ public void GermanIsSingleton() var actual = Numeral.German; Assert.Same(expected, actual); } + + [Fact] + public void BrazilianIsCorrect() + { + var actual = Numeral.Brazilian; + Assert.IsAssignableFrom(actual); + } + + [Fact] + public void BrazilianIsSingleton() + { + var expected = Numeral.Brazilian; + var actual = Numeral.Brazilian; + Assert.Same(expected, actual); + } } } diff --git a/Numsense.UnitTests/BrazilianPortugueseExamples.fs b/Numsense.UnitTests/BrazilianPortugueseExamples.fs new file mode 100644 index 0000000..4eacf2c --- /dev/null +++ b/Numsense.UnitTests/BrazilianPortugueseExamples.fs @@ -0,0 +1,302 @@ +module Ploeh.Numsense.BrazilianPortugueseExamples + +open Xunit +open Swensen.Unquote + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``tryParseBrazilian returns correct result`` (portuguese, expected) = + let actual = Numeral.tryParseBrazilian portuguese + Some expected =! actual + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``toBrazilian returns correct result`` (i, expected) = + let actual = Numeral.toBrazilian i + expected =! actual + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``tryParseBrazilian with incorrect input returns correct result`` (portuguese) = + let actual = Numeral.tryParseBrazilian portuguese + None =! actual diff --git a/Numsense.UnitTests/NumeralProperties.fs b/Numsense.UnitTests/NumeralProperties.fs index d176158..38ce9e6 100644 --- a/Numsense.UnitTests/NumeralProperties.fs +++ b/Numsense.UnitTests/NumeralProperties.fs @@ -200,3 +200,18 @@ let ``negative German is the inverse of positive German`` x = sprintf "minus-%s" (Numeral.toGerman x) =! actualGerman Some -x =! actualInteger + +[] +let ``tryParseBrazilian is the inverse of toBrazilian`` x = + test <@ Some x = (x |> Numeral.toBrazilian |> Numeral.tryParseBrazilian) @> + +[] +let ``negative Brazilian is the inverse of positive Brazilian`` x = + x <> 0 ==> lazy + let x = abs x + + let actualBrazilian = Numeral.toBrazilian -x + let actualInteger = Numeral.tryParseBrazilian actualBrazilian + + sprintf "menos %s" (Numeral.toBrazilian x) =! actualBrazilian + Some -x =! actualInteger diff --git a/Numsense.UnitTests/Numsense.UnitTests.fsproj b/Numsense.UnitTests/Numsense.UnitTests.fsproj index e60610c..01c3fce 100644 --- a/Numsense.UnitTests/Numsense.UnitTests.fsproj +++ b/Numsense.UnitTests/Numsense.UnitTests.fsproj @@ -54,6 +54,7 @@ + diff --git a/Numsense/BrazilianPortuguese.fs b/Numsense/BrazilianPortuguese.fs new file mode 100644 index 0000000..4cad088 --- /dev/null +++ b/Numsense/BrazilianPortuguese.fs @@ -0,0 +1,136 @@ +module internal Ploeh.Numsense.BrazilianPortuguese + +open Ploeh.Numsense.InternalDsl + +let rec internal toBrazilianImp x = + let formatPrefix prefix factor x = + let remainder = x % factor + + let rec hundreds x = + if x > 1000 + then hundreds (x / 1000) + else x + + match remainder, hundreds remainder with + | 0, _ -> prefix + | _, r when r > 100 && r % 100 <> 0 -> + sprintf "%s, %s" prefix <| toBrazilianImp remainder + | _ -> sprintf "%s e %s" prefix <| toBrazilianImp remainder + + let formatSuffix suffix factor x = + let prefix = sprintf "%s%s" (toBrazilianImp (x / factor)) suffix + formatPrefix prefix factor x + + match x with + | x when x < 0 -> sprintf "menos %s" <| toBrazilianImp -x + | 0 -> "zero" + | 1 -> "um" + | 2 -> "dois" + | 3 -> "três" + | 4 -> "quatro" + | 5 -> "cinco" + | 6 -> "seis" + | 7 -> "sete" + | 8 -> "oito" + | 9 -> "nove" + | 10 -> "dez" + | 11 -> "onze" + | 12 -> "doze" + | 13 -> "treze" + | 14 -> "quatorze" + | 15 -> "quinze" + | 16 -> "dezesseis" + | 17 -> "dezessete" + | 18 -> "dezoito" + | 19 -> "dezenove" + | Between 20 30 x -> formatPrefix "vinte" 10 x + | Between 30 40 x -> formatPrefix "trinta" 10 x + | Between 40 50 x -> formatPrefix "quarenta" 10 x + | Between 50 60 x -> formatPrefix "cinquenta" 10 x + | Between 60 70 x -> formatPrefix "sessenta" 10 x + | Between 70 80 x -> formatPrefix "setenta" 10 x + | Between 80 90 x -> formatPrefix "oitenta" 10 x + | Between 90 100 x -> formatPrefix "noventa" 10 x + | 100 -> "cem" + | Between 100 200 x -> formatPrefix "cento" 100 x + | Between 200 300 x -> formatPrefix "duzentos" 100 x + | Between 300 400 x -> formatPrefix "trezentos" 100 x + | Between 500 600 x -> formatPrefix "quinhentos" 100 x + | Between 400 1000 x -> formatSuffix "centos" 100 x + | Between 1000 2000 x -> formatPrefix "mil" 1000 x + | Between 1000 1000000 x -> formatSuffix " mil" 1000 x + | Between 1000000 2000000 x -> formatPrefix "um milhão" 1000000 x + | Between 2000000 1000000000 x -> formatSuffix " milhões" 1000000 x + | Between 1000000000 2000000000 x -> formatPrefix "um bilhão" 1000000000 x + | _ -> formatSuffix " bilhões" 1000000000 x + +let internal tryParseBrazilianImp (x : string) = + let rec conv acc candidate = + let conv' remainderFactor placeFactor acc' candidate = + if (acc % remainderFactor) / placeFactor <> 0 + then None + else conv acc' candidate + + let convBillions acc' candidate = + if acc / 1000000000 <> 0 + then None + else conv acc' candidate + + let convMillions = conv' 1000000000 1000000 + let convThousands = conv' 1000000 1000 + let convHundreds = conv' 1000 100 + let convTens = conv' 100 10 + let convUnits = conv' 10 1 + + match candidate with + | "" -> Some acc + | StartsWith " " t + | StartsWith "," t + | StartsWith "E" t -> conv acc t + | "ZERO" -> Some (0 + acc) + | StartsWith "BILHÃO" t + | StartsWith "BILHÕES" t -> convBillions (1000000000 %* acc) t + | StartsWith "MILHÃO" t + | StartsWith "MILHÕES" t -> convMillions (1000000 %* acc) t + | StartsWith "MIL" t -> convThousands (1000 %* acc) t + | StartsWith "CEM" t -> convHundreds (100 + acc) t + | StartsWith "CENTOS" t -> convHundreds (100 %* acc) t + | StartsWith "CENTO" t -> convHundreds (100 + acc) t + | StartsWith "DUZENTOS" t -> convHundreds (200 + acc) t + | StartsWith "TREZENTOS" t -> convHundreds (300 + acc) t + | StartsWith "QUINHENTOS" t -> convHundreds (500 + acc) t + | StartsWith "VINTE" t -> convTens (20 + acc) t + | StartsWith "TRINTA" t -> convTens (30 + acc) t + | StartsWith "QUARENTA" t -> convTens (40 + acc) t + | StartsWith "CINQUENTA" t -> convTens (50 + acc) t + | StartsWith "CINQÜENTA" t -> convTens (50 + acc) t + | StartsWith "SESSENTA" t -> convTens (60 + acc) t + | StartsWith "SETENTA" t -> convTens (70 + acc) t + | StartsWith "OITENTA" t -> convTens (80 + acc) t + | StartsWith "NOVENTA" t -> convTens (90 + acc) t + | StartsWith "ONZE" t -> convTens (11 + acc) t + | StartsWith "DOZE" t -> convTens (12 + acc) t + | StartsWith "TREZE" t -> convTens (13 + acc) t + | StartsWith "CATORZE" t -> convTens (14 + acc) t + | StartsWith "QUATORZE" t -> convTens (14 + acc) t + | StartsWith "QUINZE" t -> convTens (15 + acc) t + | StartsWith "DEZESSEIS" t -> convTens (16 + acc) t + | StartsWith "DEZESSETE" t -> convTens (17 + acc) t + | StartsWith "DEZOITO" t -> convTens (18 + acc) t + | StartsWith "DEZENOVE" t -> convTens (19 + acc) t + | StartsWith "DEZ" t -> convTens (10 + acc) t + | StartsWith "UM" t -> convUnits (1 + acc) t + | StartsWith "DOIS" t -> convUnits (2 + acc) t + | StartsWith "TRÊS" t -> convUnits (3 + acc) t + | StartsWith "QUATRO" t -> convUnits (4 + acc) t + | StartsWith "CINCO" t -> convUnits (5 + acc) t + | StartsWith "SEIS" t -> convUnits (6 + acc) t + | StartsWith "SETE" t -> convUnits (7 + acc) t + | StartsWith "OITO" t -> convUnits (8 + acc) t + | StartsWith "NOVE" t -> convUnits (9 + acc) t + | _ -> None + + let canonicalized = x.Trim().ToUpper(System.Globalization.CultureInfo "pt-BR") + match canonicalized with + | StartsWith "MENOS" t -> conv 0 t |> Option.map (~-) + | _ -> conv 0 canonicalized diff --git a/Numsense/Numeral.fs b/Numsense/Numeral.fs index e9a3d81..603c402 100644 --- a/Numsense/Numeral.fs +++ b/Numsense/Numeral.fs @@ -39,3 +39,6 @@ let tryParseGerman = German.tryParseGermanImp let toPortuguese = Portuguese.toPortugueseImp let tryParsePortuguese = Portuguese.tryParsePortugueseImp + +let tryParseBrazilian = BrazilianPortuguese.tryParseBrazilianImp +let toBrazilian = BrazilianPortuguese.toBrazilianImp diff --git a/Numsense/Numsense.fsproj b/Numsense/Numsense.fsproj index ff76b02..652aa90 100644 --- a/Numsense/Numsense.fsproj +++ b/Numsense/Numsense.fsproj @@ -54,6 +54,7 @@ + diff --git a/Numsense/ObjectOriented.fs b/Numsense/ObjectOriented.fs index 337e403..7b41e18 100644 --- a/Numsense/ObjectOriented.fs +++ b/Numsense/ObjectOriented.fs @@ -92,6 +92,12 @@ type PortugueseNumeralConverter () = member this.TryParse (s, result) = Helper.tryParse Numeral.tryParsePortuguese (s, &result) +type BrazilianNumeralConverter () = + interface INumeralConverter with + member this.ToNumeral number = Numeral.toBrazilian number + member this.TryParse (s, result) = + Helper.tryParse Numeral.tryParseBrazilian (s, &result) + type Numeral private () = static member val Bulgarian = BulgarianNumeralConverter () :> INumeralConverter static member val English = EnglishNumeralConverter () :> INumeralConverter @@ -106,3 +112,4 @@ type Numeral private () = static member val Romanian = RomanianNumeralConverter () :> INumeralConverter static member val German = GermanNumeralConverter () :> INumeralConverter static member val Portuguese = PortugueseNumeralConverter () :> INumeralConverter + static member val Brazilian = BrazilianNumeralConverter () :> INumeralConverter \ No newline at end of file