diff --git a/scripts/reports/count-event-codes-by-year.ts b/scripts/reports/count-event-codes-by-year.ts new file mode 100755 index 00000000000..18aadd8fd94 --- /dev/null +++ b/scripts/reports/count-event-codes-by-year.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env npx ts-node --transpile-only + +// usage: scripts/reports/count-event-codes-by-year.ts m01,m02 feew [-y 2000-2020] + +import { DateTime } from 'luxon'; +import { + ServerApplicationContext, + createApplicationContext, +} from '@web-api/applicationContext'; +import { count } from '@web-api/persistence/elasticsearch/searchClient'; +import { parseArgs } from 'node:util'; +import { parseIntsArg } from './reportUtils'; +import { requireEnvVars } from '../../shared/admin-tools/util'; +import { validateDateAndCreateISO } from '@shared/business/utilities/DateHandler'; + +requireEnvVars(['ENV', 'REGION']); + +let positionals, values; + +const config = { + allowPositionals: true, + options: { + stricken: { + default: false, + short: 's', + type: 'boolean', + }, + verbose: { + default: false, + short: 'v', + type: 'boolean', + }, + years: { + default: `${DateTime.now().toObject().year}`, + short: 'y', + type: 'string', + }, + }, + strict: true, +} as const; + +function usage(warning: string | undefined) { + if (warning) { + console.log(warning); + } + console.log(`Usage: ${process.argv[1]} M071,m074 `); + console.log('Options:', JSON.stringify(config, null, 4)); +} + +const getCountDocketEntriesByEventCodesAndYears = async ({ + applicationContext, + eventCodes, + onlyNonStricken, + years, +}: { + applicationContext: ServerApplicationContext; + eventCodes: string[]; + onlyNonStricken: boolean; + years?: number[]; +}): Promise => { + const must: {}[] = [ + { + bool: { + should: eventCodes.map(eventCode => { + return { + term: { + 'eventCode.S': eventCode, + }, + }; + }), + }, + }, + ]; + if (onlyNonStricken) { + must.push({ + term: { + 'isStricken.BOOL': false, + }, + }); + } + if (years && years.length) { + if (years.length === 1) { + must.push({ + range: { + 'receivedAt.S': { + gte: validateDateAndCreateISO({ + day: '1', + month: '1', + year: String(years[0]), + }), + lt: validateDateAndCreateISO({ + day: '1', + month: '1', + year: String(years[0] + 1), + }), + }, + }, + }); + } else { + must.push({ + bool: { + should: years.map(year => { + return { + range: { + 'receivedAt.S': { + gte: validateDateAndCreateISO({ + day: '1', + month: '1', + year: String(year), + }), + lt: validateDateAndCreateISO({ + day: '1', + month: '1', + year: String(year + 1), + }), + }, + }, + }; + }), + }, + }); + } + } + const searchParameters = { + body: { + query: { + bool: { + must, + }, + }, + }, + index: 'efcms-docket-entry', + }; + // console.log('Effective Query:', JSON.stringify(searchParameters, null, 4)); + + const results = await count({ + applicationContext, + searchParameters, + }); + return results; +}; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + try { + ({ positionals, values } = parseArgs(config)); + } catch (ex) { + usage(`Error: ${ex}`); + process.exit(1); + } + if (positionals.length === 0) { + usage('invalid input: expected event codes'); + process.exit(1); + } + const eventCodes = positionals[0].split(',').map(s => s.toUpperCase()); + const years: number[] = parseIntsArg(values.years); + const includeStricken = values.stricken; + const ret = await getCountDocketEntriesByEventCodesAndYears({ + applicationContext: createApplicationContext({}), + eventCodes, + onlyNonStricken: !includeStricken, + years, + }); + if (values.verbose) { + usage('Verbose output enabled'); + console.log(`positionals: ${positionals}`); + console.log(`option values: ${values}`); + } + console.log(ret); +})(); diff --git a/scripts/reports/event-codes-by-year.ts b/scripts/reports/event-codes-by-year.ts old mode 100644 new mode 100755 index 426915dbcf7..66a707a71ed --- a/scripts/reports/event-codes-by-year.ts +++ b/scripts/reports/event-codes-by-year.ts @@ -1,6 +1,11 @@ -// usage: npx ts-node --transpile-only scripts/reports/event-codes-by-year.ts M071,M074 2021-2022 > ~/Desktop/m071s-and-m074s-filed-2021-2022.csv +#!/usr/bin/env npx ts-node --transpile-only +// usage: scripts/reports/event-codes-by-year.ts M071,M074 [-y 2021-2022] > ~/Desktop/m071s-and-m074s-filed-2021-2022.csv + +import { DateTime } from 'luxon'; import { createApplicationContext } from '@web-api/applicationContext'; +import { parseArgs } from 'node:util'; +import { parseIntsArg } from './reportUtils'; import { requireEnvVars } from '../../shared/admin-tools/util'; import { search, @@ -9,6 +14,27 @@ import { import { validateDateAndCreateISO } from '@shared/business/utilities/DateHandler'; requireEnvVars(['ENV', 'REGION']); +let positionals, values; + +const config = { + allowPositionals: true, + options: { + years: { + default: `${DateTime.now().toObject().year}`, + short: 'y', + type: 'string', + }, + }, + strict: true, +} as const; + +function usage(warning: string | undefined) { + if (warning) { + console.log(warning); + } + console.log(`Usage: ${process.argv[1]} M071,m074 [-y 2023,2024]`); + console.log('Options:', JSON.stringify(config, null, 4)); +} const cachedCases: { [key: string]: RawCase } = {}; @@ -132,22 +158,19 @@ const getDocketEntriesByEventCodesAndYears = async ({ // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { - const applicationContext = createApplicationContext({}); - - const eventCodes = process.argv[2].split(','); - const years: number[] = []; - const yearArg = process.argv[3]; - if (yearArg.includes('-')) { - const [lower, upper] = yearArg.split('-'); - for (let i = Number(lower); i <= Number(upper); i++) { - years.push(Number(i)); - } - } else { - const yearStrings = yearArg.split(','); - for (const year of yearStrings) { - years.push(Number(year)); - } + try { + ({ positionals, values } = parseArgs(config)); + } catch (ex) { + usage(`Error: ${ex}`); + process.exit(1); } + if (positionals.length === 0) { + usage('invalid input: expected event codes'); + process.exit(1); + } + const eventCodes = positionals[0].split(',').map(s => s.toUpperCase()); + const years: number[] = parseIntsArg(values.years); + const applicationContext = createApplicationContext({}); const docketEntries = await getDocketEntriesByEventCodesAndYears({ applicationContext, diff --git a/scripts/reports/reportUtils.test.ts b/scripts/reports/reportUtils.test.ts new file mode 100644 index 00000000000..3ae6473f068 --- /dev/null +++ b/scripts/reports/reportUtils.test.ts @@ -0,0 +1,55 @@ +import { parseIntRange, parseInts, parseIntsArg } from './reportUtils'; + +describe('parseIntsRange', () => { + it('returns array when given valid ranges', () => { + expect(parseIntRange('1-2')).toEqual([1, 2]); + expect(parseIntRange('3-1')).toEqual([1, 2, 3]); + expect(parseIntRange('1-3')).toEqual([1, 2, 3]); + expect(parseIntRange('0-0')).toEqual([0]); // hoot! + expect(parseIntRange('')).toEqual([]); + }); + + it('returns single item array when given single number', () => { + expect(parseIntRange('1')).toEqual([1]); + }); + + it('returns empty array when given no valid input', () => { + expect(parseIntRange('')).toEqual([]); + }); +}); + +describe('parseInts', () => { + it('should return empty array when given empty input', () => { + expect(parseInts('')).toEqual([]); + }); + it('should ignore trailing comma', () => { + expect(parseInts('1,')).toEqual([1]); + }); + it('should return an array when given comma-delimited list', () => { + expect(parseInts('1,2,3')).toEqual([1, 2, 3]); + }); + it('should return an array when given tab-delimited list', () => { + expect(parseInts('1\t2\t3', '\t')).toEqual([1, 2, 3]); + }); + + it('should return array of ints', () => { + let ints = parseInts('1,2,3'); + expect(ints).toEqual([1, 2, 3]); + ints.forEach(n => { + expect(typeof n).toBe('number'); + }); + }); +}); + +describe('parseIntsArg', () => { + it('returns empty array when given empty input', () => { + expect(parseIntsArg('')).toEqual([]); + }); + it('handles int ranges', () => { + expect(parseIntsArg('1-3')).toEqual([1, 2, 3]); + }); + + it('handles int lists', () => { + expect(parseIntsArg('1,2,3')).toEqual([1, 2, 3]); + }); +}); diff --git a/scripts/reports/reportUtils.ts b/scripts/reports/reportUtils.ts new file mode 100644 index 00000000000..0642abe4d11 --- /dev/null +++ b/scripts/reports/reportUtils.ts @@ -0,0 +1,35 @@ +export function parseInts(ints: string, delimiter = ','): number[] { + let nums = ints + .split(delimiter) + .filter(s => s.length) + .map(s => parseInt(s)); + return nums; +} + +export function parseIntRange(intRange: string): number[] { + const ints = intRange + .split('-') + .filter(s => s.length) + .map(s => parseInt(s)); + const min = Math.min(...ints); + const max = Math.max(...ints); + let rangeNums: number[] = []; + for (let i = min; i <= max; i++) { + rangeNums.push(i); + } + return rangeNums; +} + +// eslint-disable-next-line spellcheck/spell-checker +/** + * + * @param intstr a string containing an int (e.g. '1'), list of ints(e.g. '1,2,3'), or inclusive int range (e.g. '1-3') + * @returns an array of one or more integers + */ +export function parseIntsArg(intstr: string): number[] { + if (intstr.indexOf('-') > 0) { + return parseIntRange(intstr); + } else { + return parseInts(intstr); + } +}