-
Notifications
You must be signed in to change notification settings - Fork 129
[2.4.0 stack 3/7] Fixes across existing utils #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
81f397f
fix(currency): parse real decimal/thousands separators and never form…
hyanmandian f489f53
fix(ie): accept SP produtor rural and Tocantins 11-digit registrations
hyanmandian 6852e86
fix(cep): reject non-digit characters and drop the dead widenet provider
hyanmandian bf7a4d0
fix(states): sort by pt-BR locale and return fresh, mutation-safe copies
hyanmandian 9d4ac02
fix(cities): return fresh copies and cache the combined list
hyanmandian 78ffccc
fix(license-plate): strip mask characters and add Mercosul conversion
hyanmandian 7ed931e
fix(processo-juridico): use the CNJ Resolução 65/2008 dot-separated mask
hyanmandian 40bd8a9
fix(capitalize): match user word lists case-insensitively and treat -…
hyanmandian 630beaf
fix(passport): accept lowercase and mask characters
hyanmandian 414efb7
fix(pis): accept alphanumeric-style separators and avoid repeated-dig…
hyanmandian 3a6d005
fix(legal-nature): replace the table with IBGE/CONCLA Natureza Jurídi…
hyanmandian c719d10
fix(holidays): audit every state holiday against its law
hyanmandian dab3ddb
fix(holidays): isHoliday returns a boolean and never throws
hyanmandian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { writeFile } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { inflateSync } from "node:zlib"; | ||
|
|
||
| import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; | ||
|
|
||
| const scriptsDir = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| const SOURCE_URL = | ||
| "https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf"; | ||
|
|
||
| const SOURCE_PAGE_URL = | ||
| "https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021"; | ||
|
|
||
| const OUTPUT_PATH = "./src/is-valid-legal-nature/constants.ts"; | ||
|
|
||
| const EXPECTED_CODES = 92; | ||
|
|
||
| const LEGACY_LEGAL_NATURE: Record<string, string> = { | ||
| "2076": "Sociedade Empresária em Nome Coletivo", | ||
| "2100": "Sociedade Mercantil de Capital e Indústria (extinta pelo NCC/2002)", | ||
| "2208": "Entidade Binacional Itaipu", | ||
| "3042": "Organização Social", | ||
| "3050": "Organização da Sociedade Civil de Interesse Público (Oscip)", | ||
| "3093": "Unidade Executora (Programa Dinheiro Direto na Escola)", | ||
| "3123": "Partido Político", | ||
| "5002": "Organização Internacional e Outras Instituições Extraterritoriais", | ||
| }; | ||
|
|
||
| const TYPO_FIXES: Record<string, string> = { | ||
| "Frente Plebiscitária ou Referendaria": "Frente Plebiscitária ou Referendária", | ||
| }; | ||
|
|
||
| const inflateStreams = (pdf: Buffer): string[] => { | ||
| const streams: string[] = []; | ||
| let cursor = 0; | ||
|
|
||
| while (cursor < pdf.length) { | ||
| const start = pdf.indexOf("stream", cursor); | ||
| if (start < 0) break; | ||
|
|
||
| let contentStart = start + "stream".length; | ||
| if (pdf[contentStart] === 0x0d) contentStart += 1; | ||
| if (pdf[contentStart] === 0x0a) contentStart += 1; | ||
|
|
||
| const end = pdf.indexOf("endstream", contentStart); | ||
| if (end < 0) break; | ||
|
|
||
| try { | ||
| streams.push(inflateSync(pdf.subarray(contentStart, end)).toString("latin1")); | ||
| } catch {} | ||
|
|
||
| cursor = end + "endstream".length; | ||
| } | ||
|
|
||
| return streams; | ||
| }; | ||
|
|
||
| const unescapePdfString = (value: string): string => | ||
| value.replace(/\\([0-7]{1,3})|\\(.)/g, (_match, octal?: string, char?: string) => { | ||
| if (octal) return String.fromCharCode(Number.parseInt(octal, 8)); | ||
| if (char === "n") return "\n"; | ||
| if (char === "r") return "\r"; | ||
| if (char === "t") return "\t"; | ||
| return char ?? ""; | ||
| }); | ||
|
|
||
| const extractLines = (streams: string[]): string[] => { | ||
| const lines: string[] = []; | ||
|
|
||
| for (const stream of streams) { | ||
| const rows = new Map<number, Array<[number, string]>>(); | ||
|
|
||
| for (const block of stream.matchAll(/BT([\s\S]*?)ET/g)) { | ||
| const body = block[1]; | ||
| const matrix = [...body.matchAll(/([-\d.]+)\s+([-\d.]+)\s+Tm/g)].pop(); | ||
| if (!matrix) continue; | ||
|
|
||
| let text = ""; | ||
| for (const array of body.matchAll(/\[((?:[^[\]\\]|\\.)*)\]\s*TJ/g)) { | ||
| for (const chunk of array[1].matchAll(/\(((?:[^()\\]|\\.)*)\)/g)) { | ||
| text += unescapePdfString(chunk[1]); | ||
| } | ||
| } | ||
| for (const chunk of body.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { | ||
| text += unescapePdfString(chunk[1]); | ||
| } | ||
| if (!text) continue; | ||
|
|
||
| const x = Number.parseFloat(matrix[1]); | ||
| const y = Math.round(Number.parseFloat(matrix[2]) * 10) / 10; | ||
|
|
||
| const row = rows.get(y) ?? []; | ||
| row.push([x, text]); | ||
| rows.set(y, row); | ||
| } | ||
|
|
||
| for (const y of [...rows.keys()].sort((a, b) => b - a)) { | ||
| const row = rows.get(y) ?? []; | ||
| lines.push( | ||
| row | ||
| .sort(([a], [b]) => a - b) | ||
| .map(([, text]) => text) | ||
| .join(""), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return lines; | ||
| }; | ||
|
|
||
| const parseLegalNatures = (lines: string[]): Record<string, string> => { | ||
| const legalNatures: Record<string, string> = {}; | ||
|
|
||
| for (const line of lines) { | ||
| const match = line.match(/^\s*(\d{3})-(\d)\s*-\s*(.+?)\s*$/); | ||
| if (!match) continue; | ||
|
|
||
| const code = `${match[1]}${match[2]}`; | ||
| const description = match[3].replace(/\s+/g, " ").trim(); | ||
|
|
||
| legalNatures[code] = TYPO_FIXES[description] ?? description; | ||
| } | ||
|
|
||
| return legalNatures; | ||
| }; | ||
|
|
||
| const stringifyEntries = (entries: Record<string, string>): string => | ||
| Object.entries(entries) | ||
| .map(([code, description]) => `\t${JSON.stringify(code)}: ${JSON.stringify(description)},`) | ||
| .join("\n"); | ||
|
|
||
| const main = async () => { | ||
| const response = await fetchWithRetry(SOURCE_URL); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`CONCLA legal natures request failed with status ${response.status}`); | ||
| } | ||
|
|
||
| const pdf = Buffer.from(await response.arrayBuffer()); | ||
| const current = parseLegalNatures(extractLines(inflateStreams(pdf))); | ||
| const codes = Object.keys(current); | ||
|
|
||
| if (codes.length !== EXPECTED_CODES) { | ||
| throw new Error(`Expected ${EXPECTED_CODES} legal natures, got ${codes.length}`); | ||
| } | ||
|
|
||
| const legacy = Object.fromEntries( | ||
| Object.entries(LEGACY_LEGAL_NATURE).filter(([code]) => !(code in current)), | ||
| ); | ||
|
|
||
| await writeFile( | ||
| resolve(scriptsDir, "..", OUTPUT_PATH), | ||
| `/** | ||
| * Tabela de Natureza Jurídica 2021 (IBGE/CONCLA), indexed by the four digit code. | ||
| * | ||
| * Generated by \`node ./scripts/legal-natures.ts\`. Do not edit by hand. | ||
| * | ||
| * @see ${SOURCE_PAGE_URL} | ||
| * @see ${SOURCE_URL} | ||
| */ | ||
| export const LEGAL_NATURE: Record<string, string> = { | ||
| ${stringifyEntries(current)} | ||
|
|
||
| ${stringifyEntries(legacy)} | ||
| }; | ||
| `, | ||
| ); | ||
| }; | ||
|
|
||
| await main().catch((error) => { | ||
| console.error(error instanceof Error ? error.message : error); | ||
| process.exit(1); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { describe, expect, test } from "../test/runtime"; | ||
| import { clampPrecision } from "./clamp-precision"; | ||
|
|
||
| describe("clampPrecision", () => { | ||
| test("should default to 2", () => { | ||
| expect(clampPrecision()).toBe(2); | ||
| expect(clampPrecision(undefined)).toBe(2); | ||
| }); | ||
|
|
||
| test("should default to 2 when it is not a finite number", () => { | ||
| expect(clampPrecision(Number.NaN)).toBe(2); | ||
| expect(clampPrecision(Number.POSITIVE_INFINITY)).toBe(2); | ||
| // @ts-expect-error | ||
| expect(clampPrecision("3")).toBe(2); | ||
| }); | ||
|
|
||
| test("should keep valid precisions", () => { | ||
| expect(clampPrecision(0)).toBe(0); | ||
| expect(clampPrecision(3)).toBe(3); | ||
| expect(clampPrecision(20)).toBe(20); | ||
| expect(clampPrecision(20)).toBe(20); | ||
| }); | ||
|
|
||
| test("should clamp out of range precisions", () => { | ||
| expect(clampPrecision(-1)).toBe(0); | ||
| expect(clampPrecision(-100)).toBe(0); | ||
| expect(clampPrecision(21)).toBe(20); | ||
| expect(clampPrecision(1000)).toBe(20); | ||
| }); | ||
|
|
||
| test("should truncate fractional precisions", () => { | ||
| expect(clampPrecision(2.9)).toBe(2); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: brazilian-utils/javascript
Length of output: 16652
Validate the IBGE response before generation.
If the successful payload is not an array,
json.sortthrows. If a state lacksregiao, the mapping at line 38 throws. TheState[]annotation does not validateresponse.json()at runtime. Parse the payload asunknownand validate the array and required fields before writingstates.ts. Add fixtures for both invalid shapes.🤖 Prompt for AI Agents