Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,5 @@ ASALocalRun/
.DS_Store

# dotnet-releaser
artifacts-dotnet-releaser/
artifacts-dotnet-releaser/
/.claude
21 changes: 19 additions & 2 deletions BrazilianUtils.Tests/CnpjTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ open Xunit
[<InlineData"46.238.497/0001-81">]
[<InlineData"18240603000126">]
[<InlineData"77973689000165">]
// Alphanumeric CNPJs (Instrução Normativa RFB nº 2.119)
[<InlineData"12.ABC.345/01DE-35">]
[<InlineData"12ABC34501DE35">]
let shouldBeValid cnpj =
cnpj
|> Cnpj.IsValid
Expand All @@ -18,10 +21,12 @@ let shouldBeValid cnpj =
[<InlineData null>]
[<InlineData"123456">]
[<InlineData"11257245286">]
[<InlineData"abcabcabcde">]
[<InlineData"11111111111">]
[<InlineData"77973689000163">]
[<InlineData"77173389000163">]
// Invalid alphanumeric CNPJs
[<InlineData"12.ABC.345/01DE-99">]
[<InlineData"AAAAAAAAAAAA00">]
let shouldBeInvalid cnpj =
cnpj
|> Cnpj.IsValid
Expand All @@ -30,11 +35,23 @@ let shouldBeInvalid cnpj =
[<Theory>]
[<InlineData"22938962000129">]
[<InlineData"42.999.072/0001-34">]
let shouldFormattedStringMatchRegex cnpj =
let shouldFormattedNumericStringMatchRegex cnpj =
Assert.Matches(@"^[0-9]{2}(\.[0-9]{3}){2}\/[0-9]{4}\-[0-9]{2}$", Cnpj.Format cnpj)

[<Theory>]
[<InlineData"12ABC34501DE35">]
[<InlineData"12.ABC.345/01DE-35">]
let shouldFormattedAlphanumericStringMatchRegex cnpj =
Assert.Matches(@"^[0-9A-Z]{2}(\.[0-9A-Z]{3}){2}\/[0-9A-Z]{4}\-[0-9]{2}$", Cnpj.Format cnpj)

[<Fact>]
let generatedCnpjShouldBeValid() =
[ 1..100 ]
|> Seq.forall (fun _ -> Cnpj.Generate() |> Cnpj.IsValid)
|> Assert.True

[<Fact>]
let generatedAlphanumericCnpjShouldBeValid() =
[ 1..100 ]
|> Seq.forall (fun _ -> Cnpj.GenerateAlphanumeric() |> Cnpj.IsValid)
|> Assert.True
41 changes: 38 additions & 3 deletions BrazilianUtils/Cnpj.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module BrazilianUtils.Cnpj

open Helpers
open System
open System.Text

let private cnpjLength = 14
Expand Down Expand Up @@ -34,14 +35,38 @@ let private isValidSecondCheckDigit (value : int list) =

let private hasCnpjLength = hasLength cnpjLength

// Alphanumeric CNPJ support (Instrução Normativa RFB nº 2.119)
// Converts character to CNPJ value: ASCII code - 48
// Numbers 0-9 map to 0-9, letters A-Z map to 17-42
let private charToCnpjValue (c : char) =
int (Char.ToUpper c) - 48

let private isValidCnpjChar (c : char) =
Char.IsDigit c || (Char.ToUpper c >= 'A' && Char.ToUpper c <= 'Z')

let private onlyAlphanumeric (value : string) =
value
|> String.filter isValidCnpjChar

let private stringToCnpjValues (value : string) =
value
|> onlyAlphanumeric
|> Seq.map charToCnpjValue
|> Seq.toList

let private isNotRepdigitCnpj (value : int list) =
value
|> Seq.forall (fun elem -> elem = value.[0])
|> not

// Visible members
let IsValid cnpj =
let clearValue = stringToIntList cnpj
[ hasValue; hasCnpjLength; isNotRepdigit; isValidFirstCheckDigit; isValidSecondCheckDigit ]
let clearValue = stringToCnpjValues cnpj
[ hasValue; hasCnpjLength; isNotRepdigitCnpj; isValidFirstCheckDigit; isValidSecondCheckDigit ]
|> List.forall (fun validator -> validator clearValue)

let Format cnpj =
let clearValue = OnlyNumbers cnpj
let clearValue = onlyAlphanumeric cnpj
StringBuilder(clearValue).Insert(2, ".").Insert(6, ".").Insert(10, "/").Insert(15, "-").ToString()

let Generate () =
Expand All @@ -51,3 +76,13 @@ let Generate () =
baseCnpj @ [firstCheckDigit] @ [secondCheckDigit]
|> List.map (fun x -> x.ToString())
|> String.concat ""

let GenerateAlphanumeric () =
let rnd = Random()
let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let generateAlphanumericChar () = chars.[rnd.Next(chars.Length)]
let baseCnpj = List.init (cnpjLength - 2) (fun _ -> generateAlphanumericChar())
let baseCnpjValues = baseCnpj |> List.map charToCnpjValue
let firstCheckDigit = calculateDigit firstCheckDigitWeights baseCnpjValues
let secondCheckDigit = calculateDigit secondCheckDigitWeights (baseCnpjValues@[firstCheckDigit])
(baseCnpj |> List.map string |> String.concat "") + firstCheckDigit.ToString() + secondCheckDigit.ToString()