-
Notifications
You must be signed in to change notification settings - Fork 0
finances: add rogers and refactor #14
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
Changes from all commits
7be0f4e
8c7ddc6
e769e6c
187f8c1
d2f425e
9c20937
b8fde71
7ac64fa
fb15f14
ad69000
0988cc4
d073dcb
5c64865
dac6913
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { isReadPdfError, readPdf } from "./pdf_read.ts"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { isDocumentParseError } from "./document.ts"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { identify, isIdentifyError } from "./identify.ts"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface CalculateFileNamesError { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "CalculateFileNamesError"; | ||||||||||||||||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||||||||||||||||
| filePath: string; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export function isCalculateFileNamesError( | ||||||||||||||||||||||||||||||||||||||||||||
| e: unknown, | ||||||||||||||||||||||||||||||||||||||||||||
| ): e is CalculateFileNamesError { | ||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| e !== null && | ||||||||||||||||||||||||||||||||||||||||||||
| typeof e === "object" && | ||||||||||||||||||||||||||||||||||||||||||||
| "type" in e && | ||||||||||||||||||||||||||||||||||||||||||||
| e.type === "CalculateFileNamesError" && | ||||||||||||||||||||||||||||||||||||||||||||
| "message" in e && | ||||||||||||||||||||||||||||||||||||||||||||
| typeof e.message === "string" && | ||||||||||||||||||||||||||||||||||||||||||||
| "filePath" in e && | ||||||||||||||||||||||||||||||||||||||||||||
| typeof e.filePath === "string" | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export async function calculateFileNames( | ||||||||||||||||||||||||||||||||||||||||||||
| filePaths: string[], | ||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Map<string, string> | CalculateFileNamesError> { | ||||||||||||||||||||||||||||||||||||||||||||
| const infoByFileName = new Map< | ||||||||||||||||||||||||||||||||||||||||||||
| string, | ||||||||||||||||||||||||||||||||||||||||||||
| Array<{ filePath: string; hash: string }> | ||||||||||||||||||||||||||||||||||||||||||||
| >(); | ||||||||||||||||||||||||||||||||||||||||||||
| const fileNameByFilePath = new Map<string, string>(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| for (const filePath of filePaths) { | ||||||||||||||||||||||||||||||||||||||||||||
| if (fileNameByFilePath.has(filePath)) { | ||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const readPdfResult = await readPdf(filePath); | ||||||||||||||||||||||||||||||||||||||||||||
| if (isReadPdfError(readPdfResult)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "CalculateFileNamesError", | ||||||||||||||||||||||||||||||||||||||||||||
| message: readPdfResult.message, | ||||||||||||||||||||||||||||||||||||||||||||
| filePath, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const document = identify(readPdfResult.lines); | ||||||||||||||||||||||||||||||||||||||||||||
| if (typeof document === "undefined") { | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "CalculateFileNamesError", | ||||||||||||||||||||||||||||||||||||||||||||
| message: "unable to identify pdf contents", | ||||||||||||||||||||||||||||||||||||||||||||
| filePath, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| if (isIdentifyError(document)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "CalculateFileNamesError", | ||||||||||||||||||||||||||||||||||||||||||||
| message: document.message, | ||||||||||||||||||||||||||||||||||||||||||||
| filePath, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const parseResult = document.parse(readPdfResult.lines); | ||||||||||||||||||||||||||||||||||||||||||||
| if (isDocumentParseError(parseResult)) { | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| type: "CalculateFileNamesError", | ||||||||||||||||||||||||||||||||||||||||||||
| message: parseResult.message, | ||||||||||||||||||||||||||||||||||||||||||||
| filePath, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||||||||||||||||||||||||||||
| const fileName = document.calculateFileName(parseResult as unknown as any); | ||||||||||||||||||||||||||||||||||||||||||||
| fileNameByFilePath.set(filePath, fileName); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using an unsafe 'as unknown as any' cast and disabling ESLint rules, we can use a generic helper function 'parseAndCalculate' to parse and calculate the file name in a 100% type-safe manner.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const newFileNameInfo = { filePath, hash: readPdfResult.hash }; | ||||||||||||||||||||||||||||||||||||||||||||
| const info = infoByFileName.get(fileName); | ||||||||||||||||||||||||||||||||||||||||||||
| if (info) { | ||||||||||||||||||||||||||||||||||||||||||||
| info.push(newFileNameInfo); | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| infoByFileName.set(fileName, [newFileNameInfo]); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| for (const [fileName, infoList] of infoByFileName.entries()) { | ||||||||||||||||||||||||||||||||||||||||||||
| const hashes = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||||
| for (const info of infoList) { | ||||||||||||||||||||||||||||||||||||||||||||
| hashes.add(info.hash); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (hashes.size < 2) { | ||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const numberByHash = new Map<string, number>(); | ||||||||||||||||||||||||||||||||||||||||||||
| for (const hash of hashes) { | ||||||||||||||||||||||||||||||||||||||||||||
| numberByHash.set(hash, numberByHash.size + 1); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorting the hashes before assigning numbers ensures that the numbering is completely deterministic and independent of the order in which the files are passed to the command.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const fileNameByHash = new Map<string, string>(); | ||||||||||||||||||||||||||||||||||||||||||||
| for (const [hash, fileNumber] of numberByHash) { | ||||||||||||||||||||||||||||||||||||||||||||
| const lastPeriodIndex = fileName.lastIndexOf("."); | ||||||||||||||||||||||||||||||||||||||||||||
| let numberedFileName: string; | ||||||||||||||||||||||||||||||||||||||||||||
| if (lastPeriodIndex < 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| numberedFileName = `${fileName} ${fileNumber}`; | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| numberedFileName = | ||||||||||||||||||||||||||||||||||||||||||||
| fileName.substring(0, lastPeriodIndex) + | ||||||||||||||||||||||||||||||||||||||||||||
| ` ${fileNumber}` + | ||||||||||||||||||||||||||||||||||||||||||||
| fileName.substring(lastPeriodIndex); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| fileNameByHash.set(hash, numberedFileName); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| for (const info of infoList) { | ||||||||||||||||||||||||||||||||||||||||||||
| const newFileName = fileNameByHash.get(info.hash); | ||||||||||||||||||||||||||||||||||||||||||||
| if (!newFileName) { | ||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||
| `internal error kwteqzzx79: ` + | ||||||||||||||||||||||||||||||||||||||||||||
| `fileNameByHash.get(info.hash) ` + | ||||||||||||||||||||||||||||||||||||||||||||
| `returned ${Bun.inspect(newFileName)}`, | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| fileNameByFilePath.set(info.filePath, newFileName); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return fileNameByFilePath; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Define the generic helper function 'parseAndCalculate' to safely handle the parsing and file name calculation without losing type safety.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { | ||
| calculateFileNames, | ||
| isCalculateFileNamesError, | ||
| } from "../calculate_file_names.ts"; | ||
|
|
||
| export interface FilenameOptions { | ||
| v?: boolean; | ||
| } | ||
|
|
||
| export async function filenameCommand( | ||
| filePaths: string | string[], | ||
| options?: FilenameOptions, | ||
| ): Promise<void> { | ||
| if (typeof filePaths === "string") { | ||
| return filenameCommand([filePaths]); | ||
| } | ||
|
|
||
| const outputFileNameByFilePath = await calculateFileNames(filePaths); | ||
| if (isCalculateFileNamesError(outputFileNameByFilePath)) { | ||
| const { message, filePath } = outputFileNameByFilePath; | ||
| console.error(`ERROR: ${message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| for (const filePath of filePaths) { | ||
| const outputFileName = outputFileNameByFilePath.get(filePath); | ||
| if (!outputFileName) { | ||
| throw new Error( | ||
| `internal error patvap56xt: ` + | ||
| `outputFileNameByFilePath.get(${Bun.inspect(filePath)}) ` + | ||
| `returned ${Bun.inspect(outputFileName)}`, | ||
| ); | ||
| } | ||
|
|
||
| if (options?.v) { | ||
| console.log(`${filePath}: ${outputFileName}`); | ||
| } else { | ||
| console.log(outputFileName); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { isReadPdfError, readPdf } from "../pdf_read.ts"; | ||
| import { identify, isIdentifyError } from "../identify.ts"; | ||
|
|
||
| export interface IdentifyOptions { | ||
| v?: boolean; | ||
| } | ||
|
|
||
| export async function identifyCommand( | ||
| filePath: string | string[], | ||
| options?: IdentifyOptions, | ||
| ): Promise<void> { | ||
| if (Array.isArray(filePath)) { | ||
| for (const currentFilePath of filePath) { | ||
| await identifyCommand(currentFilePath, options); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const readPdfResult = await readPdf(filePath); | ||
| if (isReadPdfError(readPdfResult)) { | ||
| console.error(`ERROR: ${readPdfResult.message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const document = identify(readPdfResult.lines); | ||
| if (isIdentifyError(document)) { | ||
| const { message } = document; | ||
| console.error(`ERROR: ${message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const type: string = document?.type ?? "<unknown>"; | ||
|
|
||
| if (options?.v) { | ||
| console.log(`${filePath}: ${type}`); | ||
| } else { | ||
| console.log(type); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { isReadPdfError, readPdf } from "../pdf_read.ts"; | ||
| import { isDocumentParseError } from "../document.ts"; | ||
| import { identify, isIdentifyError } from "../identify.ts"; | ||
|
|
||
| export interface ParseOptions { | ||
| v?: boolean; | ||
| } | ||
|
|
||
| export async function parseCommand( | ||
| filePath: string | string[], | ||
| options?: ParseOptions, | ||
| ): Promise<void> { | ||
| if (Array.isArray(filePath)) { | ||
| for (const currentFilePath of filePath) { | ||
| await parseCommand(currentFilePath, options); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const readPdfResult = await readPdf(filePath); | ||
| if (isReadPdfError(readPdfResult)) { | ||
| console.error(`ERROR: ${readPdfResult.message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const document = identify(readPdfResult.lines); | ||
| if (typeof document === "undefined") { | ||
| console.error(`ERROR: unable to identify pdf contents: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
| if (isIdentifyError(document)) { | ||
| const { message } = document; | ||
| console.error(`ERROR: ${message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const parseResult = document.parse(readPdfResult.lines); | ||
| if (isDocumentParseError(parseResult)) { | ||
| const { message } = parseResult; | ||
| console.error(`ERROR: ${message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (options?.v) { | ||
| console.log(filePath); | ||
| } | ||
| console.log(parseResult); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { isReadPdfError, readPdf } from "../pdf_read.ts"; | ||
|
|
||
| export interface PrintOptions { | ||
| v?: boolean; | ||
| } | ||
|
|
||
| export async function printCommand( | ||
| filePath: string | string[], | ||
| options?: PrintOptions, | ||
| ): Promise<void> { | ||
| if (Array.isArray(filePath)) { | ||
| for (const currentFilePath of filePath) { | ||
| await printCommand(currentFilePath, options); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const readPdfResult = await readPdf(filePath); | ||
| if (isReadPdfError(readPdfResult)) { | ||
| console.error(`ERROR: ${readPdfResult.message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (options?.v) { | ||
| console.log(filePath); | ||
| } | ||
| console.log(readPdfResult.text); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import * as fs from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
|
|
||
| import { | ||
| calculateFileNames, | ||
| isCalculateFileNamesError, | ||
| } from "../calculate_file_names.ts"; | ||
|
|
||
| export interface RenameOptions { | ||
| v?: boolean; | ||
| } | ||
|
|
||
| export async function renameCommand( | ||
| filePaths: string | string[], | ||
| options?: RenameOptions, | ||
| ): Promise<void> { | ||
| if (typeof filePaths === "string") { | ||
| return renameCommand([filePaths]); | ||
| } | ||
|
|
||
| const outputFileNameByFilePath = await calculateFileNames(filePaths); | ||
| if (isCalculateFileNamesError(outputFileNameByFilePath)) { | ||
| const { message, filePath } = outputFileNameByFilePath; | ||
| console.error(`ERROR: ${message}: ${filePath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const renamedPaths = new Set<string>(); | ||
| let skippedCount = 0; | ||
|
|
||
| for (const srcFilePath of filePaths) { | ||
| const destFileName = outputFileNameByFilePath.get(srcFilePath); | ||
| if (!destFileName) { | ||
| throw new Error( | ||
| `internal error xwttprzh98: ` + | ||
| `outputFileNameByFilePath.get(${Bun.inspect(srcFilePath)}) ` + | ||
| `returned ${Bun.inspect(destFileName)}`, | ||
| ); | ||
| } | ||
|
|
||
| if (renamedPaths.has(srcFilePath)) { | ||
| console.log( | ||
| `Skipping renaming ${srcFilePath} to ${destFileName} (already processed)`, | ||
| ); | ||
| skippedCount++; | ||
| continue; | ||
| } | ||
|
|
||
| const srcFileName = path.basename(srcFilePath); | ||
| if (srcFileName === destFileName) { | ||
| console.log( | ||
| `Skipping renaming ${srcFilePath} (already has the correct file name)`, | ||
| ); | ||
| skippedCount++; | ||
| continue; | ||
| } | ||
|
|
||
| const dir = path.dirname(srcFilePath); | ||
| const destFilePath = path.join(dir, destFileName); | ||
|
|
||
| if (options?.v) { | ||
| console.log(`Renaming ${srcFilePath} to ${destFileName}`); | ||
| } else { | ||
| console.log(destFilePath); | ||
| } | ||
|
|
||
| await fs.rename(srcFilePath, destFilePath); | ||
| renamedPaths.add(srcFilePath); | ||
|
Comment on lines
+61
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent accidental data loss, check if the destination file already exists before renaming. 'fs.rename' will silently overwrite the target file if it exists, which can lead to lost files if multiple source files map to the same name or if a file with that name was already present. let destExists = false;
try {
await fs.access(destFilePath);
destExists = true;
} catch {
// Destination does not exist, safe to rename
}
if (destExists) {
console.error(
"ERROR: Destination file already exists, skipping to prevent overwrite: " + destFilePath,
);
skippedCount++;
continue;
}
if (options?.v) {
console.log("Renaming " + srcFilePath + " to " + destFileName);
} else {
console.log(destFilePath);
}
await fs.rename(srcFilePath, destFilePath);
renamedPaths.add(srcFilePath); |
||
| } | ||
|
|
||
| console.log(`${renamedPaths.size} files renamed (${skippedCount} skipped)`); | ||
| } | ||
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.
Import 'Document' and 'ParsedDocument' types to enable a fully type-safe helper function for parsing and calculating file names, avoiding unsafe 'any' casts.