diff --git a/Numsense.UnitTests.CSharp/NumeralTests.cs b/Numsense.UnitTests.CSharp/NumeralTests.cs index 8572dea..b8cb841 100644 --- a/Numsense.UnitTests.CSharp/NumeralTests.cs +++ b/Numsense.UnitTests.CSharp/NumeralTests.cs @@ -62,8 +62,9 @@ public void FarsiIsSingleton() { var expected = Numeral.Farsi; var actual = Numeral.Farsi; - Assert.Same(expected, actual); - } + + Assert.Same(expected, actual); + } public void PolishIsCorrect() { @@ -93,7 +94,22 @@ public void DutchIsSingleton() var actual = Numeral.Dutch; Assert.Same(expected, actual); } - + + [Fact] + public void PortugueseIsCorrect() + { + var actual = Numeral.Portuguese; + Assert.IsAssignableFrom(actual); + } + + [Fact] + public void PortugueseIsSingleton() + { + var expected = Numeral.Portuguese; + var actual = Numeral.Portuguese; + Assert.Same(expected, actual); + } + [Fact] public void RussianIsCorrect() { diff --git a/Numsense.UnitTests.CSharp/Numsense.UnitTests.CSharp.csproj b/Numsense.UnitTests.CSharp/Numsense.UnitTests.CSharp.csproj index 3079b73..a3c7f5f 100644 --- a/Numsense.UnitTests.CSharp/Numsense.UnitTests.CSharp.csproj +++ b/Numsense.UnitTests.CSharp/Numsense.UnitTests.CSharp.csproj @@ -97,4 +97,4 @@ --> - \ No newline at end of file + diff --git a/Numsense.UnitTests/EnglishExamples.fs b/Numsense.UnitTests/EnglishExamples.fs index 97cefc9..9628f30 100644 --- a/Numsense.UnitTests/EnglishExamples.fs +++ b/Numsense.UnitTests/EnglishExamples.fs @@ -241,4 +241,4 @@ let ``tryParseEnglish returns correct result`` (english, expected) = "two-billion-one-hundred-forty-seven-million-four-hundred-eighty-three-thousand-six-hundred-forty-seven")>] let ``toEnglish returns correct result`` (i, expected) = let actual = Numeral.toEnglish i - expected =! actual \ No newline at end of file + expected =! actual diff --git a/Numsense.UnitTests/NumeralProperties.fs b/Numsense.UnitTests/NumeralProperties.fs index 5fd0813..d176158 100644 --- a/Numsense.UnitTests/NumeralProperties.fs +++ b/Numsense.UnitTests/NumeralProperties.fs @@ -94,6 +94,21 @@ let ``negative Dutch is the inverse of positive Dutch`` x = sprintf "min %s" (Numeral.toDutch x) =! actualDutch Some -x =! actualInteger +[] +let ``tryParsePortuguese is the inverse of toPortuguese`` x = + test <@ Some x = (x |> Numeral.toPortuguese |> Numeral.tryParsePortuguese) @> + +[] +let ``negative Portuguese is the inverse of positive Portuguese`` x = + x <> 0 ==> lazy + let x = abs x + + let actualPortuguese = Numeral.toPortuguese -x + let actualInteger = Numeral.tryParsePortuguese actualPortuguese + + sprintf "menos %s" (Numeral.toPortuguese x) =! actualPortuguese + Some -x =! actualInteger + [] let ``tryParseRussian is the inverse of toRussian`` x = test <@ Some x = (x |> Numeral.toRussian |> Numeral.tryParseRussian) @> diff --git a/Numsense.UnitTests/Numsense.UnitTests.fsproj b/Numsense.UnitTests/Numsense.UnitTests.fsproj index 0328e7b..e60610c 100644 --- a/Numsense.UnitTests/Numsense.UnitTests.fsproj +++ b/Numsense.UnitTests/Numsense.UnitTests.fsproj @@ -61,6 +61,7 @@ + diff --git a/Numsense.UnitTests/PortugueseExamples.fs b/Numsense.UnitTests/PortugueseExamples.fs new file mode 100644 index 0000000..444548d --- /dev/null +++ b/Numsense.UnitTests/PortugueseExamples.fs @@ -0,0 +1,218 @@ +module Ploeh.Numsense.PortugueseExamples + +open Xunit +open Swensen.Unquote + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``tryParsePortuguese returns correct result`` (portuguese, expected) = + let actual = Numeral.tryParsePortuguese portuguese + Some expected =! actual + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] + +let ``toPortuguese returns correct result`` (i, expected) = + let actual = Numeral.toPortuguese i + expected =! actual diff --git a/Numsense.UnitTests/packages.config b/Numsense.UnitTests/packages.config index 72b92eb..34121cd 100644 --- a/Numsense.UnitTests/packages.config +++ b/Numsense.UnitTests/packages.config @@ -9,4 +9,5 @@ + \ No newline at end of file diff --git a/Numsense/Numeral.fs b/Numsense/Numeral.fs index 06fb93c..e9a3d81 100644 --- a/Numsense/Numeral.fs +++ b/Numsense/Numeral.fs @@ -4,35 +4,38 @@ module Ploeh.Numsense.Numeral let toBulgarian = Bulgarian.toBulgarianImp let tryParseBulgarian = Bulgarian.tryParseBulgarianImp -let toDanish = Danish.toDanishImp 1 -let tryParseDanish = Danish.tryParseDanishImp +let toDanish = Danish.toDanishImp 1 +let tryParseDanish = Danish.tryParseDanishImp -let toEnglish = English.toEnglishImp -let tryParseEnglish = English.tryParseEnglishImp +let toEnglish = English.toEnglishImp +let tryParseEnglish = English.tryParseEnglishImp -let toFarsi = Farsi.toFarsiImp -let tryParseFarsi = Farsi.tryParseFarsiImp +let toFarsi = Farsi.toFarsiImp +let tryParseFarsi = Farsi.tryParseFarsiImp -let toPolish = Polish.toPolishImp -let tryParsePolish = Polish.tryParsePolishImp +let toPolish = Polish.toPolishImp +let tryParsePolish = Polish.tryParsePolishImp -let toDutch = Dutch.toDutchImp -let tryParseDutch = Dutch.tryParseDutchImp +let toDutch = Dutch.toDutchImp +let tryParseDutch = Dutch.tryParseDutchImp -let toRussian = Russian.toRussianImp Russian.Masculine -let tryParseRussian = Russian.tryParseRussianImp +let toRussian = Russian.toRussianImp Russian.Masculine +let tryParseRussian = Russian.tryParseRussianImp -let toSpanish = Spanish.toSpanishImp -let tryParseSpanish = Spanish.tryParseSpanishImp +let toSpanish = Spanish.toSpanishImp +let tryParseSpanish = Spanish.tryParseSpanishImp -let toCatalan = Catalan.toCatalanImp -let tryParseCatalan = Catalan.tryParseCatalanImp +let toCatalan = Catalan.toCatalanImp +let tryParseCatalan = Catalan.tryParseCatalanImp -let toSwedish = Swedish.toSwedishImp -let tryParseSwedish = Swedish.tryParseSwedishImp +let toSwedish = Swedish.toSwedishImp +let tryParseSwedish = Swedish.tryParseSwedishImp -let toRomanian = Romanian.toRomanianImp -let tryParseRomanian = Romanian.tryParseRomanianImp +let toRomanian = Romanian.toRomanianImp +let tryParseRomanian = Romanian.tryParseRomanianImp -let toGerman = German.toGermanImp -let tryParseGerman = German.tryParseGermanImp +let toGerman = German.toGermanImp +let tryParseGerman = German.tryParseGermanImp + +let toPortuguese = Portuguese.toPortugueseImp +let tryParsePortuguese = Portuguese.tryParsePortugueseImp diff --git a/Numsense/Numsense.fsproj b/Numsense/Numsense.fsproj index 75c7cff..ff76b02 100644 --- a/Numsense/Numsense.fsproj +++ b/Numsense/Numsense.fsproj @@ -59,6 +59,7 @@ + diff --git a/Numsense/ObjectOriented.fs b/Numsense/ObjectOriented.fs index 2793096..337e403 100644 --- a/Numsense/ObjectOriented.fs +++ b/Numsense/ObjectOriented.fs @@ -17,7 +17,7 @@ module internal Helper = type BulgarianNumeralConverter () = interface INumeralConverter with member this.ToNumeral number = Numeral.toBulgarian number - member this.TryParse (s, result) = + member this.TryParse (s, result) = Helper.tryParse Numeral.tryParseBulgarian (s, &result) type EnglishNumeralConverter () = @@ -55,7 +55,7 @@ type SpanishNumeralConverter () = member this.ToNumeral number = Numeral.toSpanish number member this.TryParse (s, result) = Helper.tryParse Numeral.tryParseSpanish (s, &result) - + type CatalanNumeralConverter () = interface INumeralConverter with member this.ToNumeral number = Numeral.toCatalan number @@ -86,17 +86,23 @@ type GermanNumeralConverter () = member this.TryParse (s, result) = Helper.tryParse Numeral.tryParseGerman (s, &result) +type PortugueseNumeralConverter () = + interface INumeralConverter with + member this.ToNumeral number = Numeral.toPortuguese number + member this.TryParse (s, result) = + Helper.tryParse Numeral.tryParsePortuguese (s, &result) type Numeral private () = - static member val Bulgarian = BulgarianNumeralConverter () :> INumeralConverter - static member val English = EnglishNumeralConverter () :> INumeralConverter - static member val Farsi = FarsiNumeralConverter () :> INumeralConverter - static member val Danish = DanishNumeralConverter () :> INumeralConverter - static member val Polish = PolishNumeralConverter () :> INumeralConverter - static member val Dutch = DutchNumeralConverter () :> INumeralConverter - static member val Russian = RussianNumeralConverter () :> INumeralConverter - static member val Spanish = SpanishNumeralConverter () :> INumeralConverter - static member val Catalan = CatalanNumeralConverter () :> INumeralConverter - static member val Swedish = SwedishNumeralConverter () :> INumeralConverter - static member val Romanian = RomanianNumeralConverter () :> INumeralConverter - static member val German = GermanNumeralConverter () :> INumeralConverter \ No newline at end of file + static member val Bulgarian = BulgarianNumeralConverter () :> INumeralConverter + static member val English = EnglishNumeralConverter () :> INumeralConverter + static member val Farsi = FarsiNumeralConverter () :> INumeralConverter + static member val Danish = DanishNumeralConverter () :> INumeralConverter + static member val Polish = PolishNumeralConverter () :> INumeralConverter + static member val Dutch = DutchNumeralConverter () :> INumeralConverter + static member val Russian = RussianNumeralConverter () :> INumeralConverter + static member val Spanish = SpanishNumeralConverter () :> INumeralConverter + static member val Catalan = CatalanNumeralConverter () :> INumeralConverter + static member val Swedish = SwedishNumeralConverter () :> INumeralConverter + static member val Romanian = RomanianNumeralConverter () :> INumeralConverter + static member val German = GermanNumeralConverter () :> INumeralConverter + static member val Portuguese = PortugueseNumeralConverter () :> INumeralConverter diff --git a/Numsense/Portuguese.fs b/Numsense/Portuguese.fs new file mode 100644 index 0000000..9d054c6 --- /dev/null +++ b/Numsense/Portuguese.fs @@ -0,0 +1,144 @@ +module internal Ploeh.Numsense.Portuguese + +open Ploeh.Numsense.InternalDsl +open System.Diagnostics + +let rec internal toPortugueseImp x = + + // Concatenates classes of numbers and simplifies some cases like zero + let formatSimple prefix factor number = + let remainder = number % factor + + let rec getRemainderHundreds = function + | r when r > 1000 -> getRemainderHundreds (r / 1000) + | r -> r + + let remainderHundreds = remainder |> getRemainderHundreds + + let outputWithComma() = sprintf "%s, %s" prefix (toPortugueseImp remainder) + let outputWithAnd() = sprintf "%s e %s" prefix (toPortugueseImp remainder) + + match remainder, remainderHundreds with + | 0, _ -> prefix + | _, r when r > 0 && r <= 100 -> outputWithAnd() + | _, r when r > 100 && r % 100 = 0 -> outputWithAnd() + | _ -> outputWithComma() + + // Formats numbers that can be represented in different ways + let format' suffixPlural suffixSingular factor value ignoreOne = + let digits = value / factor + match digits, ignoreOne with + | 1, false -> + let prefix = sprintf "%s %s" (toPortugueseImp 1) suffixSingular + formatSimple prefix factor value + | 1, true -> + let form = if x % factor > 0 then suffixPlural else suffixSingular + formatSimple form factor value + | x, _-> + let prefix = sprintf "%s %s" (toPortugueseImp x) suffixPlural + formatSimple prefix factor value + + let formatPlural suffixPlural suffixSingular factor value = + format' suffixPlural suffixSingular factor value + + let formatSingular suffix factor value = + format' suffix suffix factor value + + match x with + | x when x < 0 -> sprintf "menos %s" (toPortugueseImp -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 -> "catorze" + | 15 -> "quinze" + | 16 -> "dezasseis" + | 17 -> "dezassete" + | 18 -> "dezoito" + | 19 -> "dezanove" + | Between 20 30 x -> formatSimple "vinte" 20 x + | Between 30 40 x -> formatSimple "trinta" 30 x + | Between 40 50 x -> formatSimple "quarenta" 40 x + | Between 50 60 x -> formatSimple "cinquenta" 50 x + | Between 60 70 x -> formatSimple "sessenta" 60 x + | Between 70 80 x -> formatSimple "setenta" 70 x + | Between 80 90 x -> formatSimple "oitenta" 80 x + | Between 90 100 x -> formatSimple "noventa" 90 x + | Between 100 200 x -> formatPlural "cento" "cem" 100 x true + | Between 200 300 x -> formatSimple "duzentos" 200 x + | Between 300 400 x -> formatSimple "trezentos" 300 x + | Between 400 500 x -> formatSimple "quatrocentos" 400 x + | Between 500 600 x -> formatSimple "quinhentos" 500 x + | Between 600 700 x -> formatSimple "seiscentos" 600 x + | Between 700 800 x -> formatSimple "setecentos" 700 x + | Between 800 900 x -> formatSimple "oitocentos" 800 x + | Between 900 1000 x -> formatSimple "novecentos" 900 x + | Between 1000 1000000 x -> formatSingular "mil" 1000 x true + | Between 1000000 1000000000 x -> formatPlural "milhões" "milhão" 1000000 x false + | _ -> formatSingular "mil milhões" 1000000000 x true + +let rec internal tryParsePortugueseImp (x:string) = + let rec conv acc candidate = + match candidate with + | "" -> Some acc + | StartsWith " " t + | StartsWith ", " t + | StartsWith "-" t + | StartsWith "E" t -> conv acc t + | StartsWith "ZERO" t -> conv (0 + acc) t + | StartsWith "UM" t -> conv (1 + acc) t + | StartsWith "DOIS" t -> conv (2 + acc) t + | StartsWith "TRÊS" t -> conv (3 + acc) t + | StartsWith "QUATRO" t -> conv (4 + acc) t + | StartsWith "CINCO" t -> conv (5 + acc) t + | StartsWith "SEIS" t -> conv (6 + acc) t + | StartsWith "OITO" t -> conv (8 + acc) t + | StartsWith "ONZE" t -> conv (11 + acc) t + | StartsWith "DOZE" t -> conv (12 + acc) t + | StartsWith "CATORZE" t + | StartsWith "QUATORZE" t -> conv (14 + acc) t + | StartsWith "QUINZE" t -> conv (15 + acc) t + | StartsWith "DEZASSEIS" t -> conv (16 + acc) t + | StartsWith "DEZASSETE" t -> conv (17 + acc) t + | StartsWith "DEZOITO" t -> conv (18 + acc) t + | StartsWith "DEZANOVE" t -> conv (19 + acc) t + | StartsWith "DEZ" t -> conv (10 + acc) t + | StartsWith "VINTE" t -> conv (20 + acc) t + | StartsWith "TRINTA" t -> conv (30 + acc) t + | StartsWith "QUARENTA" t -> conv (40 + acc) t + | StartsWith "CINQUENTA" t -> conv (50 + acc) t + | StartsWith "SESSENTA" t -> conv (60 + acc) t + | StartsWith "SETENTA" t -> conv (70 + acc) t + | StartsWith "SETE" t -> conv (7 + acc) t + | StartsWith "OITENTA" t -> conv (80 + acc) t + | StartsWith "NOVENTA" t -> conv (90 + acc) t + | StartsWith "NOVE" t -> conv (9 + acc) t + | StartsWith "CENTOS" t -> conv (100 %* acc) t + | StartsWith "DUZENTOS" t -> conv (200 + acc) t + | StartsWith "TREZENTOS" t -> conv (300 + acc) t + | StartsWith "TREZE" t -> conv (13 + acc) t + | StartsWith "QUINHENTOS" t -> conv (500 + acc) t + | StartsWith "CENTO" t + | StartsWith "CEM" t -> conv (100 + acc) t + | StartsWith "MILHÃO" t + | StartsWith "MILHÕES" t -> conv (1000000 %* acc) t + | StartsWith "MILMILHÕES" t -> + conv (if acc = 0 then 1000000000 else 1000000000 %* acc) t + | StartsWith "MIL" t -> + conv (if acc = 0 then 1000 else 1000 %* acc) t + | _ -> failwith(candidate) + + let canonicalized = x.Trim().ToUpper(System.Globalization.CultureInfo "pt-PT") + match canonicalized with + | StartsWith "MENOS" t -> conv 0 (t.Trim ()) |> Option.map ((*)-1) + | _ -> conv 0 canonicalized