[
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: '0 16 * * 5'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        language: ['go']\n        # Learn more...\n        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n      with:\n        # We must fetch at least the immediate parents so that if this is\n        # a pull request then we can checkout the head.\n        fetch-depth: 2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v3\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\non: [push, pull_request]\n\njobs:\n\n  test:\n    name: Test\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macOS-latest, windows-latest]\n        go: ['1.21.x', '1.23.x']\n\n    steps:\n\n      - name: Set git to use LF\n        run: |\n          git config --global core.autocrlf false\n          git config --global core.eol lf\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v4\n\n      - name: Set up Go ${{ matrix.go }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go }}\n        id: go\n\n      - name: Vet\n        run: go vet -v ./...\n\n      - name: Test\n        run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...\n\n      - name: Upload Coverage report\n        uses: codecov/codecov-action@v4\n        with:\n          token: ${{secrets.CODECOV_TOKEN}}\n          file: ./coverage.txt\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n.vscode/\n.idea/\n*.swp\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 caixw\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# cnregion\n\n[![Test](https://github.com/issue9/cnregion/workflows/Test/badge.svg)](https://github.com/issue9/cnregion/actions?query=workflow%3ATest)\n[![Go version](https://img.shields.io/github/go-mod/go-version/issue9/cnregion)](https://golang.org)\n[![PkgGoDev](https://pkg.go.dev/badge/github.com/issue9/cnregion)](https://pkg.go.dev/github.com/issue9/cnregion/v2)\n[![codecov](https://codecov.io/gh/issue9/cnregion/branch/master/graph/badge.svg)](https://codecov.io/gh/issue9/cnregion)\n![License](https://img.shields.io/github/license/issue9/cnregion)\n\n历年统计用区域和城乡划分代码，数据来源于 <https://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/>。\n符合国家标准 GB/T 2260 与 GB/T 10114。\n\n关于版本号，主版本号代码不兼容性更改，次版本号代码最后一次生成的数据年份，BUG 修正和兼容性的功能增加则增加修订版本号。\n\n```go\nv, err := cnregion.LoadFile(\"./data/regions.db\", \"-\", 2020)\n\np := v.Provinces() // 返回所有省列表\ncities := p[0].Items() // 返回该省下的所有市\ncounties := cities[0].Items() // 返回该市下的所有县\ntowns := counties[0].Items() // 返回所有镇\nvillages := towns[0].Items() // 所有村和街道信息\n\nd := v.Districts() // 按以前的行政大区进行划分\nprovinces := d[0].Items() // 该大区下的所有省份\n\nlist := v.Search(&SearchOptions{Text: \"温州\"}) // 按索地名中带温州的区域列表\n```\n\n对采集的数据进行了一定的加工，以减少文件的体积，文件保存在 `./data/regions.db` 中。\n\n## 安装\n\n```shell\ngo get github.com/issue9/cnregion/v2\n```\n\n## 版权\n\n本项目采用 [MIT](https://opensource.org/licenses/MIT) 开源授权许可证，完整的授权说明可在 [LICENSE](LICENSE) 文件中找到。\n"
  },
  {
    "path": "cnregion.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\n// Package cnregion 中国区域划分代码\n//\n// 中国行政区域五级划分代码，包含了省、市、县、乡和村五个级别。\n// [数据规则]以及[数据来源]。\n//\n// [数据规则]: http://www.stats.gov.cn/tjsj/tjbz/200911/t20091125_8667.html\n// [数据来源]: http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/\npackage cnregion\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n)\n\n// LoadFS 从数据文件加载数据\nfunc LoadFS(f fs.FS, file, separator string, compress bool, version ...int) (*DB, error) {\n\tdata, err := fs.ReadFile(f, file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn Load(data, separator, compress, version...)\n}\n\n// Load 将数据内容加载至 DB 对象\n//\n// version 仅加载指定年份的数据，如果为空，则加载所有数据；\nfunc Load(data []byte, separator string, compress bool, version ...int) (*DB, error) {\n\tif compress {\n\t\trd, err := gzip.NewReader(bytes.NewReader(data))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdata, err = io.ReadAll(rd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tdb := &DB{\n\t\tfullNameSeparator: separator,\n\t\tfilters:           version,\n\t}\n\tif err := db.unmarshal(data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdb.initDistricts()\n\n\treturn db, nil\n}\n\n// LoadFile 从数据文件加载数据\nfunc LoadFile(file, separator string, compress bool, version ...int) (*DB, error) {\n\tdata, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn Load(data, separator, compress, version...)\n}\n\n// Dump 输出到文件\nfunc (db *DB) Dump(file string, compress bool) error {\n\tdata, err := db.marshal()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif compress {\n\t\tbuf := new(bytes.Buffer)\n\t\tw := gzip.NewWriter(buf)\n\t\tif _, err = w.Write(data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = w.Close(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdata = buf.Bytes()\n\t}\n\n\treturn os.WriteFile(file, data, os.ModePerm)\n}\n"
  },
  {
    "path": "cnregion_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n\t\"github.com/issue9/cnregion/v2/version\"\n)\n\nfunc TestLoad(t *testing.T) {\n\ta := assert.New(t, false)\n\n\to1, err := Load(data, \"-\", false)\n\ta.NotError(err).\n\t\tEqual(o1.fullNameSeparator, obj.fullNameSeparator).\n\t\tEqual(o1.versions, obj.versions).\n\t\tEqual(len(o1.root.items), len(obj.root.items)).\n\t\tEqual(o1.root.items[0].id, obj.root.items[0].id).\n\t\tEqual(o1.root.items[0].fullID, obj.root.items[0].fullID).\n\t\tEqual(o1.root.items[0].items[0].id, obj.root.items[0].items[0].id).\n\t\tEqual(o1.root.items[1].items[0].id, obj.root.items[1].items[0].id).\n\t\tEqual(o1.root.items[1].items[0].fullID, obj.root.items[1].items[0].fullID).\n\t\tEqual(o1.root.items[1].items[1].fullID, obj.root.items[1].items[1].fullID).\n\t\tNotEqual(o1.root.items[1].items[1].fullID, obj.root.items[1].items[0].fullID)\n\n\td1, err := obj.marshal()\n\ta.NotError(err).NotNil(d1)\n\ta.Equal(string(d1), string(data))\n\n\t_, err = Load([]byte(\"100:[2020]:::1:0{}\"), \"-\", false)\n\ta.Equal(err, ErrIncompatible)\n\n\to1, err = Load(data, \"-\", false, 2019)\n\ta.NotError(err).\n\t\tEqual(0, len(o1.root.items))\n}\n\nfunc TestDB_LoadDump(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tpath := filepath.Join(os.TempDir(), \"cnregion_db.dict\")\n\ta.NotError(obj.Dump(path, false))\n\td, err := LoadFile(path, \"-\", false)\n\ta.NotError(err).NotNil(d)\n\n\tpath = filepath.Join(os.TempDir(), \"cnregion_db_compress.dict\")\n\ta.NotError(obj.Dump(path, true))\n\td, err = LoadFile(path, \"-\", true)\n\ta.NotError(err).NotNil(d)\n}\n\nfunc TestLoadFS(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tobj, err := LoadFS(os.DirFS(\"./data\"), \"regions.db\", \"-\", true)\n\ta.NotError(err).NotNil(obj)\n\ta.Equal(obj.versions, version.All()).\n\t\tEqual(obj.fullNameSeparator, \"-\").\n\t\tTrue(len(obj.root.items) > 0).\n\t\tEqual(obj.root.items[0].level, id.Province).\n\t\tEqual(obj.root.items[0].items[0].level, id.City).\n\t\tEqual(obj.root.items[0].items[0].items[0].level, id.County).\n\t\tEqual(obj.root.items[1].level, id.Province).\n\t\tEqual(obj.root.items[2].items[0].level, id.City)\n}\n"
  },
  {
    "path": "data/embed.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage data\n\nimport (\n\t\"embed\"\n\n\t\"github.com/issue9/cnregion/v2\"\n)\n\n//go:embed regions.db\nvar data embed.FS\n\n// Embed 将 regions.db 的内容嵌入到程序中\n//\n// 这样可以让程序不依赖外部文件，但同时也会增加编译后程序的大小。\nfunc Embed(separator string, version ...int) (*cnregion.DB, error) {\n\treturn cnregion.LoadFS(data, \"regions.db\", separator, true, version...)\n}\n"
  },
  {
    "path": "data/embed_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage data\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n)\n\nfunc TestEmbed(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tv, err := Embed(\">\", 2021)\n\ta.NotError(err).NotNil(v)\n\tr := v.Find(\"330305000000\")\n\ta.NotNil(r).\n\t\tEqual(r.ID(), \"05\").\n\t\tEqual(r.FullID(), \"330305000000\").\n\t\tEqual(r.Name(), \"洞头区\").\n\t\tEqual(r.FullName(), \"浙江省>温州市>洞头区\")\n}\n"
  },
  {
    "path": "db.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/issue9/errwrap\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n)\n\n// Version 数据文件的版本号\nconst Version = 1\n\n// ErrIncompatible 数据文件版本不兼容\n//\n// 当数据文件中指定的版本号与当前的 Version 不相等时，返回此错误。\nvar ErrIncompatible = errors.New(\"数据文件版本不兼容\")\n\n// DB 区域数据库信息\n//\n// 数据格式：\n//\n//\t1:[versions]:{id:name:yearIndex:size{}}\n//\n//\t- 1 表示数据格式的版本，采用当前包的 Version 常量；\n//\t- versions 表示当前数据文件中的数据支持的年份列表，以逗号分隔；\n//\t- id 当前区域的 ID；\n//\t- name 当前区域的名称；\n//\t- yearIndex 此条数据支持的年份列表，每一个位表示一个年份在 versions 中的索引值；\n//\t- size 表示子元素的数量；\ntype DB struct {\n\troot     *Region\n\tversions []int // 支持的版本\n\n\t// 以下数据不会写入数据文件中\n\n\tfullNameSeparator string\n\tdistricts         []*Region\n\n\t// Load 指定的过滤版本，仅在 unmarshal 过程中使用，\n\t// 在完成 unmarshal 之的清空。\n\tfilters []int\n}\n\n// NewDB 返回空的 [DB] 对象\nfunc NewDB() *DB {\n\tdb := &DB{versions: []int{}}\n\tdb.root = &Region{db: db}\n\treturn db\n}\n\n// Version 当前这份数据支持的年份列表\nfunc (db *DB) Versions() []int { return db.versions }\n\n// AddVersion 添加新的版本号\nfunc (db *DB) AddVersion(ver int) (ok bool) {\n\tif slices.Index(db.versions, ver) > -1 { // 检测 ver 是否已经存在\n\t\treturn false\n\t}\n\n\tdb.versions = append(db.versions, ver)\n\treturn true\n}\n\n// Find 查找指定 ID 对应的信息\nfunc (db *DB) Find(regionID string) *Region { return db.root.findItem(id.SplitFilter(regionID)...) }\n\nvar levelIndex = []id.Level{id.Province, id.City, id.County, id.Town, id.Village}\n\n// AddItem 添加一条子项\nfunc (db *DB) AddItem(regionID, name string, ver int) error {\n\tlist := id.SplitFilter(regionID)\n\titem := db.root.findItem(list...)\n\n\tif item == nil {\n\t\titems := list[:len(list)-1] // 上一级\n\t\titem = db.root.findItem(items...)\n\t\tlevel := levelIndex[len(items)]\n\t\treturn item.addItem(list[len(list)-1], name, level, ver)\n\t}\n\n\treturn item.setSupported(ver)\n}\n\nfunc (db *DB) marshal() ([]byte, error) {\n\tversions := make([]string, 0, len(db.versions))\n\tfor _, v := range db.versions {\n\t\tversions = append(versions, strconv.Itoa(v))\n\t}\n\n\tbuf := errwrap.Buffer{Buffer: bytes.Buffer{}}\n\tbuf.WString(strconv.Itoa(Version)).WByte(':')\n\n\tbuf.WByte('[')\n\tbuf.WString(strings.Join(versions, \",\"))\n\tbuf.WByte(']').WByte(':')\n\n\terr := db.root.marshal(&buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif buf.Err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf.Bytes(), nil\n}\n\nfunc (db *DB) unmarshal(data []byte) error {\n\tdata, val := indexBytes(data, ':')\n\tver, err := strconv.Atoi(val)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif ver != Version {\n\t\treturn ErrIncompatible\n\t}\n\n\tdata, val = indexBytes(data, ':')\n\tversions := strings.Split(strings.Trim(val, \"[]\"), \",\")\n\tdb.versions = make([]int, 0, len(versions))\n\tfor _, version := range versions {\n\t\tv, err := strconv.Atoi(version)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdb.versions = append(db.versions, v)\n\t}\n\n\tif len(db.filters) == 0 {\n\t\tdb.filters = db.versions\n\t} else {\n\tLOOP:\n\t\tfor _, v := range db.filters {\n\t\t\tfor _, v2 := range db.versions {\n\t\t\t\tif v2 == v {\n\t\t\t\t\tcontinue LOOP\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"当前数据文件没有 %d 年份的数据\", v)\n\t\t}\n\t}\n\n\tdefer func() {\n\t\tdb.versions = db.filters\n\t\tdb.filters = db.filters[:0]\n\t}()\n\n\tdb.root = &Region{db: db}\n\treturn db.root.unmarshal(data, \"\", \"\", 0)\n}\n\nfunc (db *DB) filterVersions(versions []int) []int {\n\tvers := make([]int, 0, len(versions))\nLOOP:\n\tfor _, v := range versions {\n\t\tfor _, v2 := range db.filters {\n\t\t\tif v2 == v {\n\t\t\t\tvers = append(vers, v)\n\t\t\t\tcontinue LOOP\n\t\t\t}\n\t\t}\n\t}\n\n\treturn vers\n}\n"
  },
  {
    "path": "db_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n\t\"github.com/issue9/cnregion/v2/version\"\n)\n\nvar data = []byte(`1:[2020,2019]:::1:2{33:浙江:1:1{01:温州:3:0{}}34:安徽:1:3{01:合肥:3:0{}02:芜湖:1:0{}03:芜湖-2:1:0{}}}`)\n\nvar obj = &DB{\n\tversions:          []int{2020, 2019},\n\tfullNameSeparator: \"-\",\n\troot: &Region{\n\t\tname:     \"\",\n\t\tversions: []int{2020},\n\t\titems: []*Region{\n\t\t\t{\n\t\t\t\tid:       \"33\",\n\t\t\t\tname:     \"浙江\",\n\t\t\t\tversions: []int{2020},\n\t\t\t\tfullName: \"浙江\",\n\t\t\t\tfullID:   \"330000000000\",\n\t\t\t\tlevel:    id.Province,\n\t\t\t\titems: []*Region{\n\t\t\t\t\t{\n\t\t\t\t\t\tid:       \"01\",\n\t\t\t\t\t\tname:     \"温州\",\n\t\t\t\t\t\tversions: []int{2020, 2019},\n\t\t\t\t\t\tfullName: \"浙江-温州\",\n\t\t\t\t\t\tfullID:   \"330100000000\",\n\t\t\t\t\t\tlevel:    id.City,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid:       \"34\",\n\t\t\t\tname:     \"安徽\",\n\t\t\t\tfullName: \"安徽\",\n\t\t\t\tfullID:   \"340000000000\",\n\t\t\t\tversions: []int{2020},\n\t\t\t\tlevel:    id.Province,\n\t\t\t\titems: []*Region{\n\t\t\t\t\t{\n\t\t\t\t\t\tid:       \"01\",\n\t\t\t\t\t\tname:     \"合肥\",\n\t\t\t\t\t\tversions: []int{2020, 2019},\n\t\t\t\t\t\tfullName: \"安徽-合肥\",\n\t\t\t\t\t\tfullID:   \"340100000000\",\n\t\t\t\t\t\tlevel:    id.City,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid:       \"02\",\n\t\t\t\t\t\tname:     \"芜湖\",\n\t\t\t\t\t\tversions: []int{2020},\n\t\t\t\t\t\tfullName: \"安徽-芜湖\",\n\t\t\t\t\t\tfullID:   \"340200000000\",\n\t\t\t\t\t\tlevel:    id.City,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid:       \"03\",\n\t\t\t\t\t\tname:     \"芜湖-2\",\n\t\t\t\t\t\tversions: []int{2020},\n\t\t\t\t\t\tfullName: \"安徽-芜湖-2\",\n\t\t\t\t\t\tfullID:   \"340300000000\",\n\t\t\t\t\t\tlevel:    id.City,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc init() {\n\tsetRegionDB(obj.root, obj)\n}\n\nfunc setRegionDB(r *Region, db *DB) {\n\tr.db = db\n\tfor _, i := range r.items {\n\t\tsetRegionDB(i, db)\n\t}\n}\n\nfunc TestDB_Find(t *testing.T) {\n\ta := assert.New(t, false)\n\n\t// 2020\n\tdb, err := LoadFile(\"./data/regions.db\", \">\", true, 2020)\n\ta.NotError(err).NotNil(db)\n\tr := db.Find(\"330305000000\")\n\ta.NotNil(r).\n\t\tEqual(r.ID(), \"05\").\n\t\tEqual(r.FullID(), \"330305000000\").\n\t\tEqual(r.Name(), \"洞头区\").\n\t\tEqual(r.FullName(), \"浙江省>温州市>洞头区\").\n\t\tEqual(r.Versions(), []int{2020})\n\tr = db.Find(\"330322000000\") // 洞头县，已改为洞头区\n\ta.Nil(r)\n\n\t// 2009\n\tdb, err = LoadFile(\"./data/regions.db\", \">\", true, 2009)\n\ta.NotError(err).NotNil(db)\n\tr = db.Find(\"330322000000\")\n\ta.NotNil(r).\n\t\tEqual(r.ID(), \"22\").\n\t\tEqual(r.FullID(), \"330322000000\").\n\t\tEqual(r.Name(), \"洞头县\").\n\t\tEqual(r.FullName(), \"浙江省>温州市>洞头县\").\n\t\tEqual(r.Versions(), []int{2009})\n\tr = db.Find(\"330305000000\")\n\ta.Nil(r)\n\n\t// 所有年份的数据\n\tdb, err = LoadFile(\"./data/regions.db\", \">\", true, version.Range(2009, 2020)...)\n\ta.NotError(err).NotNil(db)\n\tr = db.Find(\"330322000000\")\n\ta.NotNil(r).\n\t\tEqual(r.ID(), \"22\").\n\t\tEqual(r.Versions(), []int{2014, 2013, 2012, 2011, 2010, 2009})\n\tr = db.Find(\"330305000000\")\n\ta.NotNil(r).\n\t\tEqual(r.ID(), \"05\").\n\t\tContains(r.Versions(), []int{2018, 2017, 2016, 2015})\n}\n"
  },
  {
    "path": "districts.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport \"github.com/issue9/cnregion/v2/id\"\n\n// Districts 按行政大区划分\n//\n// NOTE: 大区划分并不统一，按照各个省份的第一个数字进行划分。\nfunc (db *DB) Districts() []*Region { return db.districts }\n\nfunc (db *DB) initDistricts() {\n\tdb.districts = make([]*Region, 0, len(districtsMap))\n\n\tfor index, name := range districtsMap {\n\t\titems := make([]*Region, 0, 10)\n\t\tfor _, p := range db.Provinces() {\n\t\t\tif p.ID()[0] == index {\n\t\t\t\titems = append(items, p)\n\t\t\t}\n\t\t}\n\n\t\tdb.districts = append(db.districts, &Region{\n\t\t\tid:       string(index),\n\t\t\tfullID:   id.Fill(string(index), id.Village),\n\t\t\tname:     name,\n\t\t\tfullName: name,\n\t\t\titems:    items,\n\t\t})\n\t}\n\n}\n\nvar districtsMap = map[byte]string{\n\t'1': \"华北地区\",\n\t'2': \"东北地区\",\n\t'3': \"华东地区\",\n\t'4': \"中南地区\",\n\t'5': \"西南地区\",\n\t'6': \"西北地区\",\n}\n"
  },
  {
    "path": "districts_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n)\n\nfunc TestDB_Districts(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tdb, err := LoadFile(\"./data/regions.db\", \">\", true, 2020)\n\ta.NotError(err).NotNil(db)\n\ta.Length(db.Districts(), len(districtsMap))\n\n\tfor _, d := range db.Districts() {\n\t\tif d.ID() == \"1\" {\n\t\t\ta.Equal(d.Name(), \"华北地区\").Equal(5, len(d.Items()))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/issue9/cnregion/v2\n\ngo 1.21\n\nrequire (\n\tgithub.com/issue9/assert/v4 v4.3.1\n\tgithub.com/issue9/errwrap v0.3.2\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/issue9/assert/v4 v4.1.1/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4=\ngithub.com/issue9/assert/v4 v4.3.1 h1:dHYODk1yV7j/1baIB6K6UggI4r1Hfuljqic7PaDbwLg=\ngithub.com/issue9/assert/v4 v4.3.1/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4=\ngithub.com/issue9/errwrap v0.3.2 h1:7KEme9Pfe75M+sIMcPCn/DV90wjnOcRbO4DXVAHj3Fw=\ngithub.com/issue9/errwrap v0.3.2/go.mod h1:KcCLuUGiffjooLCUjL89r1cyO8/HT/VRcQrneO53N3A=\n"
  },
  {
    "path": "id/id.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\n// Package id 针对 ID 的一些操作函数\npackage id\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Level 表示区域的级别\ntype Level uint8\n\n// 对区域级别的定义\nconst (\n\tVillage Level = 1 << iota\n\tTown\n\tCounty\n\tCity\n\tProvince\n\n\tAllLevel = Village + Town + County + City + Province\n)\n\nvar lengths = map[Level]int{\n\tVillage:  12,\n\tTown:     9,\n\tCounty:   6,\n\tCity:     4,\n\tProvince: 2,\n}\n\n// Length 获取各个类型 ID 的有效果长度\nfunc Length(level Level) int {\n\tif _, found := lengths[level]; !found {\n\t\tpanic(\"无效的 level 参数\")\n\t}\n\n\treturn lengths[level]\n}\n\n// Split 将一个区域 ID 按区域进行划分\nfunc Split(id string) (province, city, county, town, village string) {\n\tif len(id) != Length(Village) {\n\t\tpanic(fmt.Sprintf(\"id 的长度只能为 %d，当前为 %s\", Length(Village), id))\n\t}\n\n\treturn id[:Length(Province)],\n\t\tid[Length(Province):Length(City)],\n\t\tid[Length(City):Length(County)],\n\t\tid[Length(County):Length(Town)],\n\t\tid[Length(Town):Length(Village)]\n}\n\n// SplitFilter 将 id 按区域进行划分且过滤掉零值的区域\n//\n//\t330312123000 => 33 03 12 123\n//\n// 如果传递的是零值，则返回空数组。\nfunc SplitFilter(id string) []string {\n\tprovince, city, county, town, village := Split(id)\n\treturn filterZero(province, city, county, town, village)\n}\n\nfunc filterZero(id ...string) []string {\n\tfor index, i := range id { // 过滤掉数组中的零值\n\t\tif isZero(i) {\n\t\t\tid = id[:index]\n\t\t\tbreak\n\t\t}\n\t}\n\treturn id\n}\n\n// Parent 获取 id 的上一级行政区域的 ID\n//\n//\t330312123456 => 330312123\nfunc Parent(id string) string {\n\tlist := SplitFilter(id)\n\treturn strings.Join(list[:len(list)-1], \"\")\n}\n\n// Prefix 获取 ID 的非零前缀\n//\n//\t330312123456 => 330312123456\n//\t330312123000 => 330312123\nfunc Prefix(id string) string {\n\treturn strings.Join(SplitFilter(id), \"\")\n}\n\n// Fill 为 id 填充后缀的 0\n//\n// id 为原始值；\n// level 为需要达到的行政级别，最终的长度为 Length(level)。\nfunc Fill(id string, level Level) string {\n\trem := Length(level) - len(id)\n\tswitch {\n\tcase rem == 0:\n\t\treturn id\n\tcase rem > Length(level) || rem < 2:\n\t\tpanic(fmt.Sprintf(\"无效的 id %s，无法为其填充 0\", id))\n\tdefault:\n\t\treturn id + strings.Repeat(\"0\", rem)\n\t}\n}\n\n// isZero 判断一组字符串是否都由 0 组成\nfunc isZero(id string) bool {\n\tfor _, r := range id {\n\t\tif r != '0' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "id/id_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage id\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n)\n\nfunc TestSplit(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tprovince, city, county, town, village := Split(\"330203103233\")\n\ta.Equal(province, \"33\").\n\t\tEqual(city, \"02\").\n\t\tEqual(county, \"03\").\n\t\tEqual(town, \"103\").\n\t\tEqual(village, \"233\")\n\n\ta.Panic(func() {\n\t\tSplit(\"3303\")\n\t})\n}\n\nfunc TestSplitFilter(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tlist := SplitFilter(\"330203103000\")\n\ta.Equal(4, len(list)).\n\t\tEqual(list[0], \"33\").\n\t\tEqual(list[1], \"02\").\n\t\tEqual(list[2], \"03\").\n\t\tEqual(list[3], \"103\")\n\n\t// 碰到第一个零值，即结果后续的判断\n\tlist = SplitFilter(\"330003103000\")\n\ta.Equal(1, len(list)).Equal(list[0], \"33\")\n\n\tlist = SplitFilter(\"000000000000\")\n\ta.Empty(list)\n}\n\nfunc TestParent(t *testing.T) {\n\ta := assert.New(t, false)\n\n\ta.Equal(Parent(\"330300000000\"), \"33\")\n\ta.Equal(Parent(\"330302111000\"), \"330302\")\n}\n\nfunc TestPrefix(t *testing.T) {\n\ta := assert.New(t, false)\n\n\ta.Equal(Prefix(\"330301001001\"), \"330301001001\")\n\ta.Equal(Prefix(\"330300000000\"), \"3303\")\n\ta.Equal(Prefix(\"330302000000\"), \"330302\")\n}\n\nfunc TestFill(t *testing.T) {\n\ta := assert.New(t, false)\n\n\ta.Equal(Fill(\"34\", Village), \"340000000000\")\n\ta.Equal(Fill(\"3\", Village), \"300000000000\")\n\ta.Equal(Fill(\"34\", Province), \"34\")\n\ta.Equal(Fill(\"34\", City), \"3400\")\n\ta.Equal(Fill(\"341234666777\", Village), \"341234666777\")\n\ta.Panic(func() {\n\t\tFill(\"34112233444332\", Village)\n\t})\n}\n\nfunc TestIsZero(t *testing.T) {\n\ta := assert.New(t, false)\n\n\ta.True(isZero(\"000\"))\n\ta.False(isZero(\"00x\"))\n\ta.True(isZero(\"\"))\n}\n"
  },
  {
    "path": "region.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/issue9/errwrap\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n)\n\n// Region 表示单个区域\ntype Region struct {\n\tid       string\n\tname     string\n\titems    []*Region\n\tversions []int // 支持的版本号列表\n\n\t// 以下数据不会写入数据文件中\n\n\tfullName string // 全名\n\tfullID   string\n\tdb       *DB\n\tlevel    id.Level\n}\n\n// Provinces 省份列表\nfunc (db *DB) Provinces() []*Region { return db.root.items }\n\nfunc (r *Region) ID() string       { return r.id }       // 区域的 ID，不包括后缀 0 和上一级的 ID\nfunc (r *Region) Name() string     { return r.name }     // 区域的名称\nfunc (r *Region) FullName() string { return r.fullName } // 区域的全称，包括上一级的名称\nfunc (r *Region) FullID() string   { return r.fullID }   // 区域的 ID，包括后缀的 0 以及上一级的 ID，长度为 12\nfunc (r *Region) Versions() []int  { return r.versions } // 支持的年份版本\nfunc (r *Region) Items() []*Region { return r.items }    // 子项\n\n// IsSupported 当前数据是否支持该年份\nfunc (r *Region) IsSupported(ver int) bool { return slices.Index(r.versions, ver) > -1 }\n\nfunc (reg *Region) addItem(id, name string, level id.Level, ver int) error {\n\tif slices.Index(reg.db.versions, ver) == -1 {\n\t\treturn fmt.Errorf(\"不支持该年份 %d 的数据\", ver)\n\t}\n\n\tfor _, item := range reg.items {\n\t\tif item.id == id {\n\t\t\treturn fmt.Errorf(\"已经存在相同 ID 的数据项：%s\", id)\n\t\t}\n\t}\n\n\treg.items = append(reg.items, &Region{\n\t\tid:       id,\n\t\tname:     name,\n\t\tdb:       reg.db,\n\t\tlevel:    level,\n\t\tversions: []int{ver},\n\t})\n\treturn nil\n}\n\nfunc (reg *Region) setSupported(ver int) error {\n\tif slices.Index(reg.db.versions, ver) == -1 {\n\t\treturn fmt.Errorf(\"不存在该年份 %d 的数据\", ver)\n\t}\n\n\tif !reg.IsSupported(ver) {\n\t\treg.versions = append(reg.versions, ver)\n\t}\n\treturn nil\n}\n\nfunc (reg *Region) findItem(regionID ...string) *Region {\n\tif len(regionID) == 0 {\n\t\treturn reg\n\t}\n\n\tfor _, item := range reg.items {\n\t\tif item.id == regionID[0] {\n\t\t\treturn item.findItem(regionID[1:]...)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (reg *Region) marshal(buf *errwrap.Buffer) error {\n\tsupported := 0\n\tfor _, ver := range reg.versions {\n\t\tindex := slices.Index(reg.db.versions, ver)\n\t\tif index == -1 {\n\t\t\treturn fmt.Errorf(\"无效的年份 %d 位于 %s\", ver, reg.fullName)\n\t\t}\n\t\tsupported += 1 << index\n\t}\n\tbuf.Printf(\"%s:%s:%d:%d{\", reg.id, reg.name, supported, len(reg.items))\n\tfor _, item := range reg.items {\n\t\terr := item.marshal(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tbuf.WByte('}')\n\n\treturn nil\n}\n\nfunc (reg *Region) unmarshal(data []byte, parentName, parentID string, level id.Level) error {\n\treg.level = level\n\n\tdata, reg.id = indexBytes(data, ':')\n\n\tdata, reg.name = indexBytes(data, ':')\n\treg.fullName = reg.name\n\tif parentName != \"\" {\n\t\treg.fullName = parentName + reg.db.fullNameSeparator + reg.name\n\t}\n\tparentID += reg.id\n\treg.fullID = id.Fill(parentID, id.Village)\n\n\t// Versions\n\tdata, val := indexBytes(data, ':')\n\tsupported, err := strconv.Atoi(val)\n\tif err != nil {\n\t\treturn err\n\t}\n\tversions := make([]int, 0, len(reg.db.versions))\n\tfor i, v := range reg.db.versions {\n\t\tif flag := 1 << i; flag&supported == flag {\n\t\t\tversions = append(versions, v)\n\t\t}\n\t}\n\treg.versions = reg.db.filterVersions(versions)\n\n\tdata, val = indexBytes(data, '{')\n\tsize, err := strconv.Atoi(val)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif size > 0 {\n\t\tfor i := 0; i < size; i++ {\n\t\t\tindex := findEnd(data)\n\t\t\tif index < 0 {\n\t\t\t\treturn errors.New(\"未找到结束符号 }\")\n\t\t\t}\n\n\t\t\t// 下一级的 Level\n\t\t\tvar next id.Level\n\t\t\tif level == 0 {\n\t\t\t\tnext = id.Province\n\t\t\t} else {\n\t\t\t\tnext = level >> 1\n\t\t\t}\n\n\t\t\titem := &Region{db: reg.db}\n\t\t\tif err := item.unmarshal(data[:index], reg.fullName, parentID, next); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(item.versions) > 0 { // 表示该条数据不支持所有的年份\n\t\t\t\treg.items = append(reg.items, item)\n\t\t\t}\n\t\t\tdata = data[index+1:]\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc indexBytes(data []byte, b byte) ([]byte, string) {\n\tindex := bytes.IndexByte(data, b)\n\tif index == -1 {\n\t\tpanic(fmt.Sprintf(\"在%s未找到：%s\", string(data), string(b)))\n\t}\n\n\treturn data[index+1:], string(data[:index])\n}\n\nfunc findEnd(data []byte) int {\n\tdeep := 0\n\tfor i, b := range data {\n\t\tswitch b {\n\t\tcase '{':\n\t\t\tdeep++\n\t\tcase '}':\n\t\t\tdeep--\n\t\t\tif deep == 0 {\n\t\t\t\treturn i\n\t\t\t}\n\t\t}\n\t}\n\n\treturn 0\n}\n"
  },
  {
    "path": "region_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n)\n\nfunc TestRegion_IsSupported(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tobj := &DB{versions: []int{2020, 2019, 2018}}\n\tobj.root = &Region{items: []*Region{\n\t\t{versions: []int{2020, 2019}, name: \"test\", db: obj},\n\t}, db: obj}\n\n\ta.True(obj.root.items[0].IsSupported(2020))\n\ta.True(obj.root.items[0].IsSupported(2019))\n\ta.False(obj.root.items[0].IsSupported(2018)) // 不支持\n\ta.False(obj.root.items[0].IsSupported(2009)) // 不存在于 db\n}\n\nfunc TestRegion_addItem(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tobj := &DB{versions: []int{2020, 2019, 2018}}\n\tobj.root = &Region{items: []*Region{}, db: obj}\n\n\ta.ErrorString(obj.root.addItem(\"33\", \"浙江\", id.Province, 2001), \"不支持该年份\")\n\n\ta.NotError(obj.root.addItem(\"44\", \"广东\", id.Province, 2020))\n\ta.Equal(obj.root.items[0].id, \"44\").\n\t\tNotNil(obj.root.items[0].db).\n\t\tTrue(obj.root.items[0].IsSupported(2020)).\n\t\tFalse(obj.root.items[0].IsSupported(2019))\n\n\ta.ErrorString(obj.root.addItem(\"44\", \"广东\", id.Province, 2020), \"存在相同\")\n}\n\nfunc TestRegion_SetSupported(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tobj := &DB{versions: []int{2020, 2019, 2018}}\n\tobj.root = &Region{items: []*Region{{db: obj}}, db: obj}\n\n\ta.NotError(obj.root.addItem(\"33\", \"浙江\", id.Province, 2020))\n\ta.NotError(obj.root.items[0].setSupported(2020)).\n\t\tEqual(obj.root.items[0].versions, []int{2020})\n\ta.NotError(obj.root.items[0].setSupported(2019)).\n\t\tEqual(obj.root.items[0].versions, []int{2020, 2019})\n\ta.ErrorString(obj.root.items[0].setSupported(2001), \"不存在该年份\")\n}\n\nfunc TestFindEnd(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tdata := []byte(\"0123{56}\")\n\ta.Equal(findEnd(data), 7)\n}\n\nfunc TestDB_Provinces(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tv, err := LoadFile(\"./data/regions.db\", \">\", true, 2020)\n\ta.NotError(err).NotNil(v)\n\n\tfor _, p := range v.Provinces() {\n\t\tif p.ID() == \"33\" {\n\t\t\ta.Equal(p.Name(), \"浙江省\")\n\t\t}\n\t}\n}\n\nfunc TestRegion_Items(t *testing.T) {\n\ta := assert.New(t, false)\n\n\t// 2020\n\tvar x05, x22 bool\n\tv, err := LoadFile(\"./data/regions.db\", \">\", true, 2020)\n\ta.NotError(err).NotNil(v)\n\tr := v.Find(\"330300000000\")\n\tfor _, item := range r.Items() {\n\t\tif item.ID() == \"05\" {\n\t\t\tx05 = true\n\t\t}\n\t\tif item.ID() == \"22\" {\n\t\t\tx22 = true\n\t\t}\n\t}\n\ta.True(x05).False(x22)\n\n\t// 2009\n\tx05 = false\n\tx22 = false\n\tv, err = LoadFile(\"./data/regions.db\", \">\", true, 2009)\n\ta.NotError(err).NotNil(v)\n\tr = v.Find(\"330300000000\")\n\tfor _, item := range r.Items() {\n\t\tif item.ID() == \"05\" {\n\t\t\tx05 = true\n\t\t}\n\t\tif item.ID() == \"22\" {\n\t\t\tx22 = true\n\t\t}\n\t}\n\ta.False(x05).True(x22)\n\n\t//2020 + 2009\n\tx05 = false\n\tx22 = false\n\tv, err = LoadFile(\"./data/regions.db\", \">\", true, 2009, 2020)\n\ta.NotError(err).NotNil(v)\n\tr = v.Find(\"330300000000\")\n\tfor _, item := range r.Items() {\n\t\tif item.ID() == \"05\" {\n\t\t\tx05 = true\n\t\t}\n\t\tif item.ID() == \"22\" {\n\t\t\tx22 = true\n\t\t}\n\t}\n\ta.True(x05).True(x22)\n}\n\nfunc TestRegion_findItem(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tr := obj.root.findItem(\"34\", \"01\")\n\ta.NotNil(r).Equal(r.name, \"合肥\").Equal(r.fullName, \"安徽-合肥\")\n\n\tr = obj.root.findItem(\"34\", \"01\", \"00\")\n\ta.Nil(r)\n\n\tr = obj.root.findItem(\"34\")\n\ta.NotNil(r).Equal(r.name, \"安徽\").Equal(r.fullName, \"安徽\")\n\n\tr = obj.root.findItem()\n\ta.NotNil(r).Equal(r.name, \"\").Equal(r.fullName, \"\").Equal(2, len(r.items))\n\n\t// 不存在于 obj\n\ta.Nil(obj.root.findItem(\"99\"))\n\ta.Nil(obj.root.findItem(\"\"))\n}\n"
  },
  {
    "path": "search.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"strings\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n)\n\n// Options 搜索选项\ntype Options struct {\n\t// 表示你需要搜索的地名需要包含的内容\n\t//\n\t// 不能是多个名称的组合，比如\"浙江温州\"，直接写\"温州\"就可以。\n\t// 也不要提供类似于\"居委会\"这种无实际意义的地名；\n\tText string\n\n\t// 上一级的区域 ID\n\t//\n\t// 为空表示不限制。\n\tParent string\n\n\t// 搜索的城市类型\n\t//\n\t// 该值取值于 github.com/issue9/cnregion/v2/id.Level 类型。 多个值可以通过或运算叠加。\n\t// 0 表示所有类型。\n\tLevel id.Level\n\n\t// 最大的搜索数量。0 表示不限制数量。\n\tMax       int\n\tunlimited bool\n}\n\nfunc (o *Options) isEmpty() bool {\n\treturn o.Text == \"\" &&\n\t\t(o.Parent == \"\" || o.Parent == \"000000000000\") &&\n\t\to.Level == 0 &&\n\t\to.Max == 0\n}\n\n// Search 简单的搜索功能\nfunc (db *DB) Search(opt *Options) []*Region {\n\tif opt == nil || opt.isEmpty() {\n\t\tpanic(\"参数 opt 不能为空值\")\n\t}\n\n\tr := db.root\n\tif opt.Parent != \"\" {\n\t\tr = db.Find(opt.Parent)\n\t}\n\tif r == nil { // 不存在 opt.Parent 指定的数据\n\t\treturn nil\n\t}\n\n\tif opt.Level == 0 {\n\t\topt.Level = id.AllLevel\n\t}\n\n\topt.unlimited = opt.Max == 0\n\tsize := 100\n\tif !opt.unlimited {\n\t\tsize = opt.Max\n\t}\n\tlist := make([]*Region, 0, size)\n\n\treturn r.search(opt, list)\n}\n\nfunc (reg *Region) search(opt *Options, list []*Region) []*Region {\n\tif strings.Contains(reg.name, opt.Text) &&\n\t\t(reg.level&opt.Level == reg.level) && reg.level != 0 { // level == 0 只有根元素才有\n\t\tlist = append(list, reg)\n\t\topt.Max--\n\t}\n\n\tif !opt.unlimited && opt.Max <= 0 {\n\t\treturn list\n\t}\n\n\tfor _, item := range reg.items {\n\t\tlist = item.search(opt, list)\n\t}\n\n\treturn list\n}\n"
  },
  {
    "path": "search_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage cnregion\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n\n\t\"github.com/issue9/cnregion/v2/id\"\n)\n\nfunc TestDB_Search(t *testing.T) {\n\ta := assert.New(t, false)\n\n\trs := obj.Search(&Options{Text: \"合肥\"})\n\ta.Equal(1, len(rs)).\n\t\tEqual(rs[0].name, \"合肥\")\n\n\trs = obj.Search(&Options{Parent: \"340000000000\", Text: \"合肥\"})\n\ta.Equal(1, len(rs)).\n\t\tEqual(rs[0].name, \"合肥\")\n\n\trs = obj.Search(&Options{Parent: \"000000000000\", Text: \"合肥\"})\n\ta.Equal(1, len(rs)).\n\t\tEqual(rs[0].name, \"合肥\")\n\n\t// 限定 level 只能是省以及 parent 为 34 开头\n\trs = obj.Search(&Options{Parent: \"340000000000\", Level: id.Province, Text: \"合肥\"})\n\ta.Equal(0, len(rs))\n\n\t// 未限定 parent 且 level 正确\n\trs = obj.Search(&Options{Level: id.City, Text: \"合肥\"})\n\ta.Equal(1, len(rs))\n\n\trs = obj.Search(&Options{Level: id.City, Text: \"湖\"})\n\ta.Equal(2, len(rs))\n\n\trs = obj.Search(&Options{Level: id.City, Parent: \"340000000000\", Text: \"湖\"})\n\ta.Equal(2, len(rs))\n\n\t// parent = 浙江\n\trs = obj.Search(&Options{Parent: \"330000000000\", Text: \"合肥\"})\n\ta.Equal(0, len(rs))\n\n\t// parent 不存在\n\trs = obj.Search(&Options{Parent: \"110000000000\", Text: \"合肥\"})\n\ta.Equal(0, len(rs))\n\n\t// 只有 Level\n\trs = obj.Search(&Options{Level: id.City})\n\ta.Equal(4, len(rs))\n\tfor _, r := range rs {\n\t\ta.True(strings.HasSuffix(r.fullID, \"00000000\"))\n\t}\n\n\t// 只有 Level\n\trs = obj.Search(&Options{Level: id.City + id.Town})\n\ta.Equal(4, len(rs))\n\n\t// 只有 Level\n\trs = obj.Search(&Options{Level: id.City + id.Province})\n\ta.Equal(6, len(rs))\n}\n\nfunc TestDB_SearchWithData(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tobj, err := LoadFS(os.DirFS(\"./data\"), \"regions.db\", \"-\", true)\n\ta.NotError(err).NotNil(obj)\n\tgot := obj.Search(&Options{Text: \"温州\"})\n\ta.NotEmpty(got)\n\n\t// Level 不匹配\n\tgot = obj.Search(&Options{Text: \"温州\", Level: id.Province})\n\ta.Empty(got)\n}\n"
  },
  {
    "path": "version/version.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\n// Package version 提供版本的相关信息\n//\n// 依据 https://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/ 提供的数据，\n// 以年作为单位进行更新，同时也以四位的年份作为版本号。\npackage version\n\nimport \"fmt\"\n\n// ErrInvalidYear 无效的年份版本\n//\n// 年份只能介于 [2009, 当前年份-1) 的区间之间。\nvar ErrInvalidYear = fmt.Errorf(\"无效的版本号，必须是介于 [%d,%d] 之间的整数\", start, latest)\n\n// 起始版本号，即提供的数据的起始年份。\nconst start = 2009\n\n// 最新的有效年份，每次更新数据之后，需要手动更新此值。\nvar latest = 2023\n\n// All 返回支持的版本号列表\nfunc All() []int { return Range(start, latest) }\n\n// IsValid 验证年份是否为一个有效的版本号\nfunc IsValid(year int) bool { return year >= start && year <= latest }\n\n// BeginWith 从 begin 开始直到最新年份\nfunc BeginWith(begin int) []int { return Range(begin, latest) }\n\n// Range 获取指定范围内的版本号\nfunc Range(begin, end int) []int {\n\tif !IsValid(begin) {\n\t\tpanic(ErrInvalidYear)\n\t}\n\n\tif !IsValid(end) {\n\t\tpanic(ErrInvalidYear)\n\t}\n\n\tif begin > end {\n\t\tpanic(ErrInvalidYear)\n\t}\n\n\tyears := make([]int, 0, end-begin+1)\n\tfor year := end; year >= begin; year-- {\n\t\tyears = append(years, year)\n\t}\n\treturn years\n}\n"
  },
  {
    "path": "version/version_test.go",
    "content": "// SPDX-FileCopyrightText: 2021-2024 caixw\n//\n// SPDX-License-Identifier: MIT\n\npackage version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/issue9/assert/v4\"\n)\n\nfunc TestAll(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tall := All()\n\t// 保证从大到小\n\ta.Equal(all[0], latest).\n\t\tEqual(all[len(all)-1], start)\n}\n\nfunc TestBeginWith(t *testing.T) {\n\ta := assert.New(t, false)\n\n\tlist := BeginWith(latest)\n\ta.Equal(1, len(list)).Equal(list[0], latest)\n\n\ta.Panic(func() {\n\t\tBeginWith(start - 1)\n\t})\n}\n"
  }
]