From 7bafec088bb1ed4a6455c196ad986d196b610010 Mon Sep 17 00:00:00 2001 From: Victor Bayas Date: Wed, 11 Nov 2026 17:42:04 -0500 Subject: [PATCH] Accept zonas no delimitadas postal codes and test against GeoNames --- CHANGELOG.md | 1 + README.md | 9 +- src/zip-code.ts | 18 +- tests/data/geonames-ec-postal-codes.txt | 1225 +++++++++++++++++++++++ tests/zip-code.test.ts | 39 +- 5 files changed, 1277 insertions(+), 15 deletions(-) create mode 100644 tests/data/geonames-ec-postal-codes.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f79ce4b..8c82346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Las reglas siguen ahora lo que publica el SRI, y algunas respuestas cambian. - `taxpayerTypeOf` dice a quién pertenece un RUC: `"persona_natural"`, `"sociedad"` (entidad pública) o `"sociedad_o_extranjero"` (tercer dígito 9, que el SRI usa para sociedades privadas y para extranjeros sin cédula). - `isNaturalPerson`, `cedulaOfRUC` y `sameTaxpayer`, para unir el RUC de una persona natural con su cédula. - `isFinalConsumer` y la constante `FINAL_CONSUMER`. +- `isZipCode` acepta los códigos de las zonas no delimitadas, que empiezan por 90 (por ejemplo, `900003`, Manga del Cura). La versión 1 los rechazaba. ### Para migrar diff --git a/README.md b/README.md index bee5501..bc9c7cd 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,15 @@ sameTaxpayer("1790016919001", "1790016919"); // false #### `isZipCode(codigo: string): boolean` -Seis dígitos que empiezan por una provincia del 01 al 24. Es independiente de las identificaciones. +Si tiene la forma de un código postal ecuatoriano: seis dígitos que empiezan por una provincia del 01 al 24, o por el 90 de las zonas no delimitadas. Es independiente de las identificaciones. ```typescript -isZipCode("170150"); // true, Pichincha +isZipCode("170102"); // true, Gualea (Quito) +isZipCode("900003"); // true, Manga del Cura ``` +Solo comprueba la forma. No hay una lista oficial abierta de los códigos que existen, así que no puede decir si un código existe: para eso, consulta a la Agencia Nacional Postal. Las pruebas comprueban que acepta los 1.225 códigos ecuatorianos que lista [GeoNames](https://www.geonames.org/). + ## Las reglas, y de dónde salen **El dígito verificador de la cédula** sigue el módulo 10 del SRI: los nueve primeros dígitos se multiplican por 2, 1, 2…, a cada producto mayor que 9 se le resta 9, y el verificador completa la suma hasta la siguiente decena, o es 0 si ya es múltiplo de 10. El tercer dígito va del 0 al 5, porque el SRI reserva el 6 y el 9 para los RUCs de entidades públicas y sociedades. @@ -131,4 +134,4 @@ Para publicar, sube una etiqueta `vX.Y.Z` que coincida con `package.json`: GitHu ## Licencia -[Apache 2.0](LICENSE) +[Apache 2.0](LICENSE). La lista de códigos postales con que se prueba `isZipCode` (`tests/data`) es de [GeoNames](https://www.geonames.org/), bajo [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). diff --git a/src/zip-code.ts b/src/zip-code.ts index 9781856..f6c2fc4 100644 --- a/src/zip-code.ts +++ b/src/zip-code.ts @@ -3,10 +3,20 @@ import { isProvince } from "./province.ts"; const SIX_DIGITS = /^\d{6}$/u; /** - * Valida un código postal ecuatoriano: seis dígitos que empiezan por una - * provincia del 01 al 24. + * Las zonas no delimitadas, territorio que dos provincias se disputaban + * (Manga del Cura, Las Golondrinas, El Piedrero), tienen códigos postales + * que empiezan por 90, como los que lista GeoNames (900001 a 900004). + */ +const UNDELIMITED_ZONES = "90"; + +/** + * Si un valor tiene la forma de un código postal ecuatoriano: seis dígitos + * que empiezan por una provincia del 01 al 24, o por el 90 de las zonas no + * delimitadas. Solo comprueba la forma: no hay una lista oficial abierta de + * los códigos que existen, así que no puede saber si uno existe. * - * @example isZipCode("170150") // true + * @example isZipCode("170102") // true, Gualea (Quito) */ export const isZipCode = (zipCode: string): boolean => - SIX_DIGITS.test(zipCode) && isProvince(zipCode); + SIX_DIGITS.test(zipCode) && + (isProvince(zipCode) || zipCode.startsWith(UNDELIMITED_ZONES)); diff --git a/tests/data/geonames-ec-postal-codes.txt b/tests/data/geonames-ec-postal-codes.txt new file mode 100644 index 0000000..a171283 --- /dev/null +++ b/tests/data/geonames-ec-postal-codes.txt @@ -0,0 +1,1225 @@ +010101 +010102 +010103 +010104 +010105 +010106 +010107 +010108 +010109 +010110 +010111 +010112 +010113 +010114 +010115 +010116 +010117 +010118 +010119 +010120 +010201 +010202 +010203 +010204 +010205 +010206 +010207 +010208 +010209 +010210 +010211 +010212 +010213 +010214 +010215 +010216 +010217 +010218 +010219 +010220 +010301 +010302 +010303 +010304 +010305 +010306 +010401 +010402 +010403 +010404 +010405 +010406 +010407 +010501 +010502 +010503 +010601 +010602 +010603 +010604 +010605 +010606 +010701 +010702 +010703 +010801 +010802 +010803 +020101 +020102 +020103 +020104 +020105 +020106 +020107 +020108 +020109 +020110 +020111 +020201 +020202 +020301 +020302 +020303 +020304 +020305 +020306 +020307 +020401 +020402 +020403 +030101 +030102 +030103 +030104 +030105 +030106 +030107 +030108 +030109 +030110 +030111 +030112 +030201 +030202 +030203 +030204 +030205 +030206 +030207 +030208 +030301 +030302 +030303 +030304 +040101 +040102 +040103 +040104 +040105 +040106 +040107 +040108 +040109 +040110 +040111 +040112 +040113 +040201 +040202 +040203 +040204 +040205 +040206 +040207 +040301 +040302 +040303 +040304 +050101 +050102 +050103 +050104 +050105 +050106 +050107 +050108 +050109 +050110 +050111 +050112 +050113 +050114 +050115 +050116 +050117 +050118 +050201 +050202 +050203 +050301 +050302 +050303 +050304 +050401 +050402 +050403 +050404 +050405 +050406 +050407 +050408 +050409 +050410 +050411 +050501 +050502 +050503 +050504 +050601 +050602 +050603 +050604 +060101 +060102 +060103 +060104 +060105 +060106 +060107 +060108 +060109 +060110 +060111 +060112 +060113 +060114 +060115 +060116 +060117 +060118 +060119 +060120 +060121 +060201 +060202 +060203 +060204 +060205 +060206 +060207 +060301 +060302 +060401 +060402 +060403 +060404 +060405 +060406 +060407 +060408 +060409 +060410 +060501 +060502 +060503 +060504 +070101 +070102 +070103 +070104 +070105 +070106 +070107 +070108 +070109 +070110 +070111 +070112 +070113 +070114 +070115 +070116 +070117 +070201 +070202 +070203 +070204 +070205 +070206 +070207 +070208 +070209 +070210 +070211 +070212 +070213 +070214 +070215 +070216 +070217 +070218 +070219 +070220 +070221 +070222 +070223 +070224 +070301 +070302 +070303 +070304 +070305 +070306 +070307 +070308 +070309 +070401 +070402 +070403 +070404 +070405 +070406 +070501 +070502 +070503 +070504 +070505 +070506 +070507 +070508 +070509 +070510 +070601 +070602 +070603 +070604 +070605 +070606 +070607 +080101 +080102 +080103 +080104 +080105 +080106 +080107 +080108 +080109 +080110 +080111 +080112 +080113 +080114 +080115 +080116 +080117 +080118 +080119 +080120 +080121 +080122 +080123 +080201 +080202 +080203 +080204 +080301 +080302 +080303 +080304 +080305 +080306 +080307 +080308 +080401 +080402 +080403 +080404 +080405 +080406 +080407 +080408 +080409 +080410 +080411 +080501 +080502 +080503 +080504 +080601 +080602 +080701 +080702 +080703 +090101 +090102 +090103 +090104 +090105 +090106 +090107 +090201 +090202 +090203 +090204 +090205 +090206 +090301 +090302 +090303 +090304 +090305 +090306 +090307 +090401 +090402 +090403 +090404 +090405 +090406 +090501 +090502 +090503 +090504 +090505 +090506 +090507 +090508 +090509 +090510 +090601 +090602 +090603 +090604 +090605 +090606 +090607 +090608 +090701 +090702 +090703 +090704 +090705 +090706 +090707 +090708 +090801 +090802 +090803 +090804 +090805 +090806 +090807 +090808 +090809 +090810 +090811 +090812 +090901 +090902 +090903 +091001 +091002 +091003 +091101 +091102 +091103 +091104 +091201 +091202 +091203 +091204 +091205 +091206 +091207 +091208 +091301 +091302 +091303 +091304 +091305 +091306 +091307 +091308 +091309 +091310 +091401 +091402 +091403 +091404 +091405 +091406 +091501 +091502 +091503 +091504 +091505 +091506 +091601 +091602 +091603 +091604 +091701 +091702 +091703 +091704 +091705 +091706 +091707 +091708 +091709 +091710 +091711 +091712 +091713 +091714 +091715 +091716 +091801 +091802 +091803 +091804 +091901 +091902 +091903 +091904 +091905 +091906 +091907 +091908 +091909 +091910 +091911 +092001 +092002 +092003 +092004 +092005 +092101 +092102 +092103 +092104 +092105 +092201 +092202 +092203 +092301 +092302 +092303 +092304 +092401 +092402 +092403 +092404 +092405 +092406 +092407 +092408 +092409 +092410 +100101 +100102 +100103 +100104 +100105 +100106 +100107 +100108 +100109 +100110 +100111 +100112 +100113 +100114 +100115 +100116 +100117 +100118 +100119 +100120 +100121 +100122 +100201 +100202 +100203 +100204 +100205 +100206 +100207 +100208 +100209 +100210 +100211 +100212 +100213 +100214 +100215 +100216 +100217 +100301 +100302 +100303 +100304 +100305 +100306 +110101 +110102 +110103 +110104 +110105 +110106 +110107 +110108 +110109 +110110 +110111 +110112 +110113 +110114 +110115 +110116 +110117 +110118 +110119 +110120 +110121 +110122 +110201 +110202 +110203 +110204 +110205 +110206 +110207 +110208 +110209 +110301 +110302 +110303 +110304 +110305 +110401 +110402 +110403 +110404 +110405 +110406 +110407 +110501 +110502 +110503 +110601 +110602 +110603 +110604 +110605 +110606 +110607 +110608 +110609 +110701 +110702 +110703 +110704 +110705 +110801 +110802 +110803 +110804 +110805 +110806 +110901 +110902 +110903 +110904 +110905 +110906 +120101 +120102 +120103 +120104 +120105 +120106 +120107 +120108 +120109 +120110 +120111 +120112 +120113 +120114 +120115 +120116 +120117 +120118 +120119 +120120 +120201 +120202 +120203 +120204 +120205 +120206 +120301 +120302 +120303 +120304 +120305 +120306 +120307 +120308 +120309 +120310 +120311 +120312 +120313 +120314 +120401 +120402 +120403 +120404 +120405 +120406 +120407 +120501 +120502 +120503 +120504 +120505 +120506 +120507 +120508 +120509 +120510 +120511 +120601 +120602 +120603 +120604 +120605 +120606 +120607 +120608 +120609 +120610 +130101 +130102 +130103 +130104 +130105 +130106 +130107 +130108 +130109 +130110 +130111 +130112 +130113 +130114 +130115 +130116 +130117 +130118 +130119 +130120 +130121 +130201 +130202 +130203 +130204 +130205 +130206 +130207 +130208 +130209 +130210 +130211 +130212 +130213 +130214 +130215 +130216 +130217 +130218 +130219 +130220 +130221 +130222 +130223 +130224 +130225 +130226 +130227 +130301 +130302 +130303 +130304 +130305 +130306 +130307 +130308 +130309 +130310 +130311 +130312 +130313 +130314 +130401 +130402 +130403 +130404 +130405 +130406 +130407 +130408 +130409 +130410 +130411 +130412 +130413 +130414 +130501 +130502 +130503 +130504 +130505 +130506 +130507 +130508 +130601 +130602 +130603 +130604 +130605 +130606 +130607 +130701 +130702 +130703 +130704 +130705 +130706 +130707 +130708 +130709 +130710 +130711 +130712 +130713 +130714 +130715 +130716 +130717 +130718 +130719 +130801 +130802 +130803 +130804 +130805 +130806 +130901 +130902 +130903 +130904 +130905 +130906 +131001 +131002 +131003 +131004 +131005 +131006 +131007 +131008 +131009 +131101 +131102 +131103 +131104 +131105 +131106 +131201 +131202 +131203 +131204 +131205 +140101 +140102 +140103 +140104 +140105 +140106 +140107 +140201 +140202 +140203 +140204 +140301 +140302 +140303 +140304 +140401 +140402 +140403 +140404 +140405 +140406 +140407 +140501 +140502 +140503 +140601 +140602 +140603 +140604 +140605 +140606 +150101 +150102 +150103 +150104 +150105 +150106 +150107 +150108 +150109 +150201 +150202 +150203 +150204 +150205 +150206 +160101 +160102 +160103 +160104 +160105 +160106 +160107 +160108 +160109 +160110 +160111 +160112 +160201 +160202 +170101 +170102 +170103 +170104 +170201 +170202 +170203 +170204 +170205 +170206 +170207 +170208 +170209 +170210 +170301 +170302 +170303 +170304 +170305 +170306 +170307 +170308 +170309 +170310 +170311 +170312 +170313 +170314 +170315 +170316 +170317 +170318 +170319 +170401 +170402 +170403 +170404 +170405 +170406 +170407 +170408 +170409 +170410 +170411 +170412 +170501 +170502 +170503 +170504 +170505 +170506 +170507 +170508 +170509 +170510 +170511 +170512 +170513 +170514 +170515 +170516 +170517 +170601 +170602 +170603 +170604 +170605 +170606 +170607 +170608 +170609 +170610 +170611 +170612 +170613 +170614 +170701 +170702 +170703 +170704 +170705 +170706 +170707 +170708 +170709 +170710 +170711 +170712 +170713 +170714 +170715 +170801 +170802 +170803 +170804 +170805 +170806 +170807 +170808 +170809 +170810 +170811 +170812 +170813 +170814 +170901 +170902 +170903 +170904 +170905 +170906 +170907 +170908 +170909 +170910 +170911 +171001 +171002 +171003 +171004 +171005 +171006 +171007 +171008 +171009 +171010 +171011 +171101 +171102 +171103 +171104 +171105 +171106 +171107 +171108 +171109 +171110 +171111 +171112 +171113 +171114 +171115 +171116 +171201 +171202 +171203 +171204 +180101 +180102 +180103 +180104 +180105 +180106 +180107 +180108 +180109 +180110 +180111 +180112 +180201 +180202 +180203 +180204 +180205 +180206 +180207 +180208 +180209 +180210 +180211 +180212 +180301 +180302 +180401 +180402 +180403 +180404 +180405 +180406 +180407 +180501 +180502 +180503 +180601 +180602 +180603 +180604 +180605 +190101 +190102 +190103 +190104 +190105 +190106 +190107 +190108 +190109 +190201 +190202 +190203 +190204 +190205 +190206 +190301 +190302 +190303 +190304 +190305 +190306 +190307 +190401 +190402 +190403 +190404 +200101 +200102 +200103 +200104 +200105 +200106 +200107 +210101 +210102 +210103 +210201 +210202 +210203 +210204 +210205 +210206 +210207 +210208 +210301 +210302 +210303 +210304 +210305 +210401 +210402 +210403 +210404 +210405 +220101 +220102 +220103 +220104 +220201 +220202 +220203 +220204 +220205 +220206 +220207 +220208 +220209 +220210 +220211 +220301 +220302 +220303 +230101 +230102 +230103 +230104 +230105 +230106 +230107 +230108 +230109 +230110 +230111 +230112 +230113 +230114 +230115 +230116 +230117 +230118 +230119 +230120 +230201 +230202 +230203 +230204 +230205 +230206 +230207 +230208 +230209 +230210 +230211 +230212 +230213 +240101 +240102 +240103 +240104 +240105 +240106 +240107 +240108 +240109 +240110 +240111 +240112 +240201 +240202 +240203 +240204 +240205 +240206 +240207 +240208 +900001 +900002 +900003 +900004 diff --git a/tests/zip-code.test.ts b/tests/zip-code.test.ts index 4971584..dcdf3ac 100644 --- a/tests/zip-code.test.ts +++ b/tests/zip-code.test.ts @@ -1,20 +1,40 @@ +import { readFileSync } from "node:fs"; import * as hegel from "@hegeldev/hegel"; import * as gs from "@hegeldev/hegel/generators"; import { describe, expect, it } from "vitest"; import { isZipCode } from "../src/index.ts"; import { digits, offByOneDigit, withNonDigit } from "./oracle.ts"; -/** Las provincias que tienen códigos postales: del 01 al 24. */ -const ZIP_PROVINCES = Array.from({ length: 24 }, (_, index) => - String(index + 1).padStart(2, "0") -); +/** + * Los códigos postales de Ecuador que lista GeoNames (EC.zip, descargado el + * 11 de noviembre de 2026, CC BY 4.0, https://www.geonames.org/). No es una + * lista completa ni oficial, pero sí independiente de este código. + */ +const GEONAMES_CODES = readFileSync( + new URL("data/geonames-ec-postal-codes.txt", import.meta.url), + "utf-8" +) + .split("\n") + .filter((line) => line.length > 0); + +/** Los prefijos que puede tener un código postal: del 01 al 24, y el 90 de las zonas no delimitadas. */ +const PREFIXES = [ + ...Array.from({ length: 24 }, (_, index) => + String(index + 1).padStart(2, "0") + ), + "90", +]; const zipCode = gs.composite( - (tc) => `${tc.draw(gs.sampledFrom(ZIP_PROVINCES))}${tc.draw(digits(4))}` + (tc) => `${tc.draw(gs.sampledFrom(PREFIXES))}${tc.draw(digits(4))}` ); describe(isZipCode, () => { - it("acepta seis dígitos que empiezan por una provincia del 01 al 24", () => { + it("acepta todo código postal que lista GeoNames", () => { + expect(GEONAMES_CODES.filter((code) => !isZipCode(code))).toStrictEqual([]); + }); + + it("acepta seis dígitos que empiezan por una provincia del 01 al 24 o por 90", () => { hegel.test((tc) => { expect(isZipCode(tc.draw(zipCode))).toBeTruthy(); }); @@ -23,15 +43,18 @@ describe(isZipCode, () => { it("rechaza seis dígitos que empiezan por cualquier otro código", () => { hegel.test((tc) => { const value = tc.draw(digits(6)); - tc.assume(!ZIP_PROVINCES.includes(value.slice(0, 2))); + tc.assume(!PREFIXES.includes(value.slice(0, 2))); expect(isZipCode(value)).toBeFalsy(); }); }); - it("rechaza todo lo que no sean exactamente seis dígitos", () => { + it("rechaza un código válido con un dígito de más o de menos", () => { hegel.test((tc) => { expect(isZipCode(tc.draw(offByOneDigit(tc.draw(zipCode))))).toBeFalsy(); }); + }); + + it("rechaza un código válido con un carácter que no es un dígito", () => { hegel.test((tc) => { expect(isZipCode(tc.draw(withNonDigit(tc.draw(zipCode))))).toBeFalsy(); });