diff --git a/README.md b/README.md index 88d9ba4..18bc937 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,11 @@ Go library for validation and generation of Russian official document codes. | OGRN | Primary State Registration Number (ОГРН) | + | + | | OGRNIP | Primary State Registration Number for IE (ОГРНИП) | + | + | | SNILS | Insurance Individual Account Number (СНИЛС) | + | + | -| OKATO | Russian Classification of Administrative Territories | - | - | +| OKATO | Russian Classification of Administrative Territories | +¹ | - | + +¹ OKATO validation checks the format and level ranges only; existence lookup and +generation are not implemented yet and return `okato.ErrNotImplemented`. OKATO is +not part of the top-level `Validate`/`Generate` dispatch — use the `okato` package directly. ## Requirements @@ -58,11 +62,15 @@ if !isValid { ```go import ( "fmt" + "log" docs_code "github.com/sshaplygin/docs-code" ) -code := docs_code.Generate(docs_code.INN) +code, err := docs_code.Generate(docs_code.INN) +if err != nil { + log.Fatal(err) +} fmt.Println("Generated INN:", code) ``` diff --git a/bik/bik_test.go b/bik/bik_test.go index 1a2897a..c96a3eb 100644 --- a/bik/bik_test.go +++ b/bik/bik_test.go @@ -42,7 +42,7 @@ func TestValidate(t *testing.T) { for i, tc := range testCases { isValid, err := Validate(tc.Code) if err != nil { - require.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + require.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { require.NoError(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -87,7 +87,7 @@ func TestValidate(t *testing.T) { for i, tc := range testCases { isValid, err := Validate(tc.Code) if err != nil { - require.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + require.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { require.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } diff --git a/bik/data.go b/bik/data.go index 7e9955b..788f3cf 100644 --- a/bik/data.go +++ b/bik/data.go @@ -3,10 +3,10 @@ package bik var countryCodes []CountryCode var supportedCountryCodes = map[CountryCode]string{ - DirectParticipationCounty: "Участник платежной системы с прямым участием", - IndirectParticipationCounty: "Участник платежной системы с косвенным участием", - NotMemberClientCBRF: "Клиент Банка России, не являющийся участником платежной системы", - RussiaCountryCode: "Код Российской Федерации", + DirectParticipationCountry: "Участник платежной системы с прямым участием", + IndirectParticipationCountry: "Участник платежной системы с косвенным участием", + NotMemberClientCBRF: "Клиент Банка России, не являющийся участником платежной системы", + RussiaCountryCode: "Код Российской Федерации", } var existsBIKs = map[string]string{ diff --git a/bik/models.go b/bik/models.go index b91ec96..84c7755 100644 --- a/bik/models.go +++ b/bik/models.go @@ -38,12 +38,12 @@ const ( unspecifiedCountryCode = "Неопределенный код страны" ) -var ( - // DirectParticipationCounty - участник платежной системы с прямым участием - DirectParticipationCounty CountryCode = 0 +const ( + // DirectParticipationCountry - участник платежной системы с прямым участием + DirectParticipationCountry CountryCode = 0 - // IndirectParticipationCounty - участник платежной системы с косвенным участием - IndirectParticipationCounty CountryCode = 1 + // IndirectParticipationCountry - участник платежной системы с косвенным участием + IndirectParticipationCountry CountryCode = 1 // NotMemberClientCBRF - клиент Банка России, не являющийся участником платежной системы NotMemberClientCBRF CountryCode = 2 @@ -158,19 +158,7 @@ type BIKStruct struct { lastNumber LastAccountNumbers } -// generateOptions TODO -type generateOptions struct { -} - -type GenerateOpt func(options *generateOptions) - -func NewBIK(opts ...GenerateOpt) *BIKStruct { - var options generateOptions - - for _, o := range opts { - o(&options) - } - +func NewBIK() *BIKStruct { return &BIKStruct{ country: GenerateCountryCode(), territoryCode: okato.GenerateStateCode(), diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..badac50 --- /dev/null +++ b/errors.go @@ -0,0 +1,7 @@ +package docs_code + +import "errors" + +// ErrUnsupportedDocType is returned by Validate and Generate when the provided +// DocType is not one of the supported document types. +var ErrUnsupportedDocType = errors.New("unsupported document type") diff --git a/fts/fts_test.go b/fts/fts_test.go new file mode 100644 index 0000000..0cf06bb --- /dev/null +++ b/fts/fts_test.go @@ -0,0 +1,70 @@ +package fts + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTaxRegionCode(t *testing.T) { + trc, err := ParseTaxRegionCode("7746") + require.NoError(t, err) + + assert.Equal(t, "7746", trc.String()) + assert.Equal(t, []int{7, 7, 4, 6}, trc.Ints()) +} + +func TestParseTaxRegionCode_Invalid(t *testing.T) { + _, err := ParseTaxRegionCode("77XX") + require.Error(t, err) +} + +func TestTaxRegionCode_Nil(t *testing.T) { + var trc *TaxRegionCode + + assert.Nil(t, trc.Ints()) + assert.False(t, trc.IsValid()) + assert.Equal(t, "0000", trc.String()) + assert.Equal(t, SupportedTaxDepartments[0].Name, trc.GetName()) +} + +func TestConstitutionRegionCode(t *testing.T) { + const adygea ConstitutionRegionCode = 1 + + assert.True(t, adygea.IsValid()) + assert.Equal(t, "01", adygea.String()) + assert.Equal(t, SupportedRegionsCodes[adygea], adygea.GetName()) + assert.Equal(t, []int{0, 1}, adygea.Ints()) +} + +func TestConstitutionRegionCode_Invalid(t *testing.T) { + const unknown ConstitutionRegionCode = 100 + + assert.False(t, unknown.IsValid()) + // Unknown/out-of-range subjects fall back to the "other territories" code. + assert.Equal(t, "99", unknown.String()) +} + +func TestRegionTaxServiceNumber_String(t *testing.T) { + assert.Equal(t, "05", RegionTaxServiceNumber(5).String()) + assert.Equal(t, "42", RegionTaxServiceNumber(42).String()) +} + +func TestGenerateConstitutionSubjectCode_AlwaysValid(t *testing.T) { + for range 100 { + require.True(t, GenerateConstitutionSubjectCode().IsValid()) + } +} + +func TestGenerateTaxRegionCode_RoundTrip(t *testing.T) { + for range 100 { + trc := GenerateTaxRegionCode() + require.NotNil(t, trc) + require.True(t, trc.IsValid()) + + parsed, err := ParseTaxRegionCode(trc.String()) + require.NoError(t, err) + assert.Equal(t, trc.Ints(), parsed.Ints()) + } +} diff --git a/generate.go b/generate.go index 2d7b16e..fa3c85e 100644 --- a/generate.go +++ b/generate.go @@ -11,7 +11,7 @@ import ( type GenerateFunc func() string -func Generate(docType DocType) string { +func Generate(docType DocType) (string, error) { var callFunc GenerateFunc switch docType { case BIK: @@ -29,8 +29,8 @@ func Generate(docType DocType) string { } if callFunc == nil { - panic("not implemented method") + return "", ErrUnsupportedDocType } - return callFunc() + return callFunc(), nil } diff --git a/generate_test.go b/generate_test.go index 482068d..a85e86c 100644 --- a/generate_test.go +++ b/generate_test.go @@ -7,7 +7,9 @@ import ( ) func Test_Generate(t *testing.T) { - inn := Generate(INN) + inn, err := Generate(INN) + require.NoError(t, err) + isValid, err := Validate(INN, inn) require.NoError(t, err) @@ -15,7 +17,6 @@ func Test_Generate(t *testing.T) { } func Test_Generate_Unsupported(t *testing.T) { - require.Panics(t, func() { - Generate(DocType(100500)) - }) + _, err := Generate(DocType(100500)) + require.ErrorIs(t, err, ErrUnsupportedDocType) } diff --git a/inn/inn.go b/inn/inn.go index 3ab40fa..442a45c 100644 --- a/inn/inn.go +++ b/inn/inn.go @@ -24,10 +24,10 @@ func Generate() string { // GenerateLegal generate legal type inn string value func GenerateLegal() string { - return NewINN(_supportedTypes[utils.Random(0, len(_supportedTypes)-1)]).String() + return NewINN(Legal).String() } // GeneratePhysical generate physical type inn string value func GeneratePhysical() string { - return NewINN(INNType(Physical)).String() + return NewINN(Physical).String() } diff --git a/inn/inn_test.go b/inn/inn_test.go index a75f4b5..1cf00f9 100644 --- a/inn/inn_test.go +++ b/inn/inn_test.go @@ -43,7 +43,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -84,7 +84,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } diff --git a/inn/models.go b/inn/models.go index 7731128..93cc082 100644 --- a/inn/models.go +++ b/inn/models.go @@ -43,6 +43,9 @@ const ( ForeignLegal ) +// foreignLegalPrefix is the fixed tax region prefix of a foreign legal-entity INN. +const foreignLegalPrefix = "9909" + var _supportedTypes = []INNType{ Physical, Legal, @@ -69,7 +72,7 @@ func (sn *SerialNumber) Ints() []int { return res } -func GenerateSerailNumber(innType INNType) SerialNumber { +func GenerateSerialNumber(innType INNType) SerialNumber { if innType == Physical { return SerialNumber{ val: int(utils.RandomDigits(physicalSerialNumberLength)), @@ -124,7 +127,13 @@ type INNStruct struct { func NewINN(innType INNType) *INNStruct { taxRegionCode := fts.GenerateTaxRegionCode() - serialNumber := GenerateSerailNumber(innType) + if innType == ForeignLegal { + // Foreign legal-entity INNs always start with the fixed 9909 prefix, + // so the random tax region code must not be used for them. + taxRegionCode, _ = fts.ParseTaxRegionCode(foreignLegalPrefix) + } + + serialNumber := GenerateSerialNumber(innType) return &INNStruct{ taxRegionCode: taxRegionCode, @@ -154,8 +163,7 @@ func ParseINN(inn string) (*INNStruct, error) { snlen = legalSerialNumberLength t = Legal parseIdx = len(inn) - 1 - const foreignLegalStartWith = "9909" - if inn[0:4] == foreignLegalStartWith { + if inn[0:4] == foreignLegalPrefix { t = ForeignLegal } } diff --git a/kpp/kpp_test.go b/kpp/kpp_test.go index da15746..edbe69e 100644 --- a/kpp/kpp_test.go +++ b/kpp/kpp_test.go @@ -47,7 +47,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -87,7 +87,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -115,7 +115,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } diff --git a/kpp/models.go b/kpp/models.go index 3599376..d8ac649 100644 --- a/kpp/models.go +++ b/kpp/models.go @@ -72,7 +72,7 @@ func ParseKPP(kpp string) (*KPPStruct, error) { return nil, fmt.Errorf("parse tax region code raw %s: %w", packageName, err) } - servialNumberArr, err := utils.StrToArr(kpp[6:]) + serialNumberArr, err := utils.StrToArr(kpp[6:]) if err != nil { return nil, fmt.Errorf("parse serial number code raw %s: %w", packageName, err) } @@ -80,7 +80,7 @@ func ParseKPP(kpp string) (*KPPStruct, error) { return &KPPStruct{ taxRegionCode: taxRegionCode, reasonCode: RegistrationReason(kpp[4:6]), - serialNumber: SerialNumber(utils.SliceToInt(servialNumberArr)), + serialNumber: SerialNumber(utils.SliceToInt(serialNumberArr)), }, nil } diff --git a/models/errors.go b/models/errors.go index 35e6939..ecb5977 100644 --- a/models/errors.go +++ b/models/errors.go @@ -22,3 +22,9 @@ type CommonError struct { func (c *CommonError) Error() string { return fmt.Sprintf("%s: %s", c.Method, c.Err.Error()) } + +// Unwrap exposes the wrapped base error so callers can use errors.Is/errors.As +// to detect sentinels such as ErrInvalidLength and ErrInvalidValue. +func (c *CommonError) Unwrap() error { + return c.Err +} diff --git a/models/errors_test.go b/models/errors_test.go new file mode 100644 index 0000000..c51f605 --- /dev/null +++ b/models/errors_test.go @@ -0,0 +1,36 @@ +package models + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommonError_Error(t *testing.T) { + err := &CommonError{Method: "inn", Err: ErrInvalidLength} + assert.Equal(t, "inn: invalid length", err.Error()) +} + +func TestCommonError_Unwrap(t *testing.T) { + base := ErrInvalidValue + err := &CommonError{Method: "bik", Err: base} + + require.Equal(t, base, err.Unwrap()) +} + +func TestCommonError_ErrorsIs(t *testing.T) { + t.Run("direct", func(t *testing.T) { + err := &CommonError{Method: "kpp", Err: ErrInvalidLength} + assert.True(t, errors.Is(err, ErrInvalidLength)) + assert.False(t, errors.Is(err, ErrInvalidValue)) + }) + + t.Run("wrapped", func(t *testing.T) { + err := fmt.Errorf("parse model: %w", &CommonError{Method: "snils", Err: ErrInvalidValue}) + assert.True(t, errors.Is(err, ErrInvalidValue)) + assert.False(t, errors.Is(err, ErrInvalidLength)) + }) +} diff --git a/ogrn/ogrn.go b/ogrn/ogrn.go index b1a2493..1e2146a 100644 --- a/ogrn/ogrn.go +++ b/ogrn/ogrn.go @@ -2,12 +2,21 @@ package ogrn import ( "fmt" + + "github.com/sshaplygin/docs-code/models" ) -// Validate check to valid OGRN format +// Validate check to valid OGRN format. It validates 13-digit OGRN codes of both +// legal entities and government bodies. Individual entrepreneur codes (OGRNIP) +// have their own ogrnip package. // example: input format is 1027700132195 func Validate(ogrn string) (bool, error) { - ogrnData, err := ParseOGRN(Legal, ogrn) + ogrnType, err := detectType(ogrn) + if err != nil { + return false, err + } + + ogrnData, err := ParseOGRN(ogrnType, ogrn) if err != nil { return false, fmt.Errorf("parse %s model: %w", packageName, err) } @@ -15,6 +24,26 @@ func Validate(ogrn string) (bool, error) { return ogrnData.IsValid() } +// detectType infers the OGRN type from the leading code digit. Physical +// (individual entrepreneur) codes are rejected: they belong to the ogrnip package. +func detectType(ogrn string) (OGRNType, error) { + if len(ogrn) == 0 { + return 0, &models.CommonError{Method: packageName, Err: models.ErrInvalidLength} + } + + first := ogrn[0] + if first < '0' || first > '9' { + return 0, &models.CommonError{Method: packageName, Err: models.ErrInvalidValue} + } + + ogrnType, ok := supportedCodes[CodeType(first-'0')] + if !ok || ogrnType == Physical { + return 0, ErrInvalidCodeType + } + + return ogrnType, nil +} + func Generate() string { return NewOGRN(Legal).String() } diff --git a/ogrn/ogrn_test.go b/ogrn/ogrn_test.go index 0ddd260..ab230cc 100644 --- a/ogrn/ogrn_test.go +++ b/ogrn/ogrn_test.go @@ -42,7 +42,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -82,12 +82,57 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } } }) + + t.Run("ogrn type detection", func(t *testing.T) { + type testCase struct { + Code string + IsValid bool + Error error + } + + testCases := []testCase{ + { + // government OGRN (leading 2) is now accepted + Code: "2027700132194", + IsValid: true, + }, + { + // government OGRN (leading 9) is now accepted + Code: "9027700132198", + IsValid: true, + }, + { + // leading 3 is an individual-entrepreneur (physical) code type + // that belongs to the ogrnip package, so it must be rejected here + Code: "3027700132195", + Error: ErrInvalidCodeType, + }, + { + Code: "", + Error: models.ErrInvalidLength, + }, + { + Code: "A027700132195", + Error: models.ErrInvalidValue, + }, + } + + for i, tc := range testCases { + isValid, err := Validate(tc.Code) + assert.Equal(t, tc.IsValid, isValid, tc.Code) + if tc.Error != nil { + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + } else { + assert.NoError(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + } + } + }) } func Test_Generate(t *testing.T) { diff --git a/ogrnip/ogrnip_test.go b/ogrnip/ogrnip_test.go index 1a8ffaf..05f73aa 100644 --- a/ogrnip/ogrnip_test.go +++ b/ogrnip/ogrnip_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/sshaplygin/docs-code/models" + "github.com/sshaplygin/docs-code/ogrn" ) func TestValidate(t *testing.T) { @@ -47,7 +48,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -73,8 +74,10 @@ func TestValidate(t *testing.T) { IsValid: false, }, { + // Valid OGRNIP length but a legal-entity code type (leading 5), + // so the underlying ogrn parser rejects it as a code-type error. Code: "512502904600034", - Error: models.ErrInvalidValue, + Error: ogrn.ErrInvalidCodeType, IsValid: false, }, { @@ -92,7 +95,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code, tc.IsValid) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } diff --git a/okato/data.go b/okato/data.go index aa73aa8..b1d3cb9 100644 --- a/okato/data.go +++ b/okato/data.go @@ -2,7 +2,7 @@ package okato var statesCodes []StateCode -var fisrtLevelCodes = map[StateCode]string{ +var firstLevelCodes = map[StateCode]string{ 0: "Территория не определена", // Территория находится за пределами России 1: "Алтайский край (г.Барнаул)", 3: "Краснодарский край (г.Краснодар)", @@ -93,8 +93,8 @@ var fisrtLevelCodes = map[StateCode]string{ } func init() { - statesCodes = make([]StateCode, 0, len(fisrtLevelCodes)) - for code := range fisrtLevelCodes { + statesCodes = make([]StateCode, 0, len(firstLevelCodes)) + for code := range firstLevelCodes { statesCodes = append(statesCodes, code) } } diff --git a/okato/errors.go b/okato/errors.go index c75cdb4..c31fe77 100644 --- a/okato/errors.go +++ b/okato/errors.go @@ -9,15 +9,18 @@ var ( // ErrInvalidCode invalid okato code ErrInvalidCode = errors.New("invalid okato code") - // ErrFirstLevelCode - ErrFirstLevelCode = errors.New("") + // ErrFirstLevelCode invalid first level (subject of the Russian Federation) code + ErrFirstLevelCode = errors.New("invalid okato first level code") - // ErrSecondLevelCode - ErrSecondLevelCode = errors.New("") + // ErrSecondLevelCode invalid second level code + ErrSecondLevelCode = errors.New("invalid okato second level code") - // ErrThirdLevelCode - ErrThirdLevelCode = errors.New("") + // ErrThirdLevelCode invalid third level code + ErrThirdLevelCode = errors.New("invalid okato third level code") - // ErrFourthLevelCode - ErrFourthLevelCode = errors.New("") + // ErrFourthLevelCode invalid fourth level code + ErrFourthLevelCode = errors.New("invalid okato fourth level code") + + // ErrNotImplemented feature is not implemented yet + ErrNotImplemented = errors.New("not implemented") ) diff --git a/okato/models.go b/okato/models.go index 09c843e..1369732 100644 --- a/okato/models.go +++ b/okato/models.go @@ -29,8 +29,8 @@ const ( ) const ( - minForuthLevelCodeLength = 0 - maxForuthLevelCodeLength = 999 + minFourthLevelCodeLength = 0 + maxFourthLevelCodeLength = 999 ) // Структура кодового обозначения в блоке идентификации: XX XXX XXX XXX, где @@ -113,7 +113,7 @@ func (ost *OKATOStruct) IsValid() (bool, error) { } func (ost *OKATOStruct) IsExist() (bool, error) { - panic("not implemented!") + return false, ErrNotImplemented } // Алейского сельсовета Алейского района Алтайского края - 01 201 802 000 @@ -125,7 +125,7 @@ func (s StateCode) IsValid() bool { } func (s StateCode) String() string { - _, ok := fisrtLevelCodes[s] + _, ok := firstLevelCodes[s] if !ok { return StateCode(0).String() } @@ -134,7 +134,7 @@ func (s StateCode) String() string { } func (s StateCode) GetName() string { - name, ok := fisrtLevelCodes[s] + name, ok := firstLevelCodes[s] if !ok { return StateCode(0).GetName() } @@ -161,5 +161,5 @@ func (s ThirdLevelCode) IsValid() bool { type FourthLevelCode int func (s FourthLevelCode) IsValid() bool { - return s >= minForuthLevelCodeLength && s <= maxForuthLevelCodeLength + return s >= minFourthLevelCodeLength && s <= maxFourthLevelCodeLength } diff --git a/okato/okato.go b/okato/okato.go index a6f4258..376fef3 100644 --- a/okato/okato.go +++ b/okato/okato.go @@ -35,6 +35,8 @@ func IsExist(bik string) (bool, error) { return okatoData.IsExist() } -func Generate() string { - panic("not implemented!") +// Generate is not implemented yet: OKATO generation requires the full +// classifier dataset and a control-number algorithm that are not available. +func Generate() (string, error) { + return "", ErrNotImplemented } diff --git a/okato/okato_test.go b/okato/okato_test.go index 17f7d7b..9bd2487 100644 --- a/okato/okato_test.go +++ b/okato/okato_test.go @@ -43,7 +43,7 @@ func Test_Validete(t *testing.T) { for i, tc := range testCases { isValid, err := Validate(tc.Code) if err != nil { - require.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + require.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { require.NoError(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -53,7 +53,6 @@ func Test_Validete(t *testing.T) { } func Test_Generate(t *testing.T) { - require.Panics(t, func() { - Generate() - }) + _, err := Generate() + require.ErrorIs(t, err, ErrNotImplemented) } diff --git a/parser/bik/main.go b/parser/bik/main.go index 664dd6d..26c5c4a 100644 --- a/parser/bik/main.go +++ b/parser/bik/main.go @@ -8,8 +8,6 @@ import ( "os" ) -var devNull *os.File - func main() { file, err := os.ReadFile("base.xml") checkErr(err) @@ -18,7 +16,7 @@ func main() { err = xml.Unmarshal(file, &biks) checkErr(err) - writer := devNull + writer := os.Stdout _, err = fmt.Fprint(writer, "package main", "\n\n") checkErr(err) @@ -65,9 +63,3 @@ func checkErr(e error) { log.Panic(e) } } - -func init() { - var err error - devNull, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0600) - checkErr(err) -} diff --git a/snils/snils_test.go b/snils/snils_test.go index 82dd678..3112493 100644 --- a/snils/snils_test.go +++ b/snils/snils_test.go @@ -47,7 +47,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } @@ -95,7 +95,7 @@ func TestValidate(t *testing.T) { isValid, err := Validate(tc.Code) assert.Equal(t, tc.IsValid, isValid, tc.Code, tc.IsValid) if err != nil { - assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) + assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } else { assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code)) } diff --git a/utils/helpers.go b/utils/helpers.go index 56f4037..ea4a2a1 100644 --- a/utils/helpers.go +++ b/utils/helpers.go @@ -10,26 +10,38 @@ import ( "github.com/sshaplygin/docs-code/models" ) -// RandomDigits generate random digits required length. Required len > 0. -func RandomDigits(len int) int64 { - if len <= 0 { - len = 1 +// RandomDigits generates a random positive integer with exactly length decimal +// digits, i.e. in the inclusive range [10^(length-1), 10^length-1]. A length < 1 +// is clamped to 1. It uses big.Int arithmetic internally so it never overflows +// for the length while producing the value. +func RandomDigits(length int) int64 { + if length < 1 { + length = 1 } - max, _ := strconv.Atoi(strings.Repeat("9", len)) - min, _ := strconv.Atoi("1" + strings.Repeat("0", len-1)) + // min = 10^(length-1), the smallest number with exactly length digits. + min := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(length-1)), nil) + // span = 9 * 10^(length-1), the count of numbers with exactly length digits. + span := new(big.Int).Mul(min, big.NewInt(9)) - num, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min+1))) - return num.Int64() + int64(min) + num, err := rand.Int(rand.Reader, span) + if err != nil { + panic(fmt.Errorf("generate random digits: %w", err)) + } + + return num.Add(num, min).Int64() } -// Random generate random digit in range [min, max]. Required max > 0. +// Random generates a uniformly distributed random integer in the inclusive +// range [min, max]. It requires min <= max, otherwise it panics. func Random(min, max int) int { - if max == 0 || min == max { - max += 1 + if min > max { + panic(fmt.Errorf("invalid random range: min %d > max %d", min, max)) } - randomNumber, err := rand.Int(rand.Reader, big.NewInt(int64(max-min+1))) + span := int64(max-min) + 1 + + randomNumber, err := rand.Int(rand.Reader, big.NewInt(span)) if err != nil { panic(fmt.Errorf("generate random number: %w", err)) } diff --git a/validate.go b/validate.go index bc4c1ce..79da73b 100644 --- a/validate.go +++ b/validate.go @@ -29,7 +29,7 @@ func Validate(docType DocType, code string) (bool, error) { } if callFunc == nil { - panic("not implemented method") + return false, ErrUnsupportedDocType } return callFunc(code) diff --git a/validate_test.go b/validate_test.go index be5400a..f0a34ea 100644 --- a/validate_test.go +++ b/validate_test.go @@ -14,8 +14,6 @@ func Test_Validate(t *testing.T) { } func Test_Validate_Unsupported(t *testing.T) { - require.Panics(t, func() { - _, err := Validate(DocType(100500), "100500") - require.NoError(t, err) - }) + _, err := Validate(DocType(100500), "100500") + require.ErrorIs(t, err, ErrUnsupportedDocType) }