/**
* Census Service
*
* Pure business logic for Census data operations.
* No external dependencies - fully testable and framework-agnostic.
*/
import {
CensusApiResponse,
CensusData,
PopulationRequest,
PopulationResult,
} from './types.js';
/**
* Parse raw Census API response into structured data
*
* @param rawResponse - Raw array response from Census API
* @returns Array of parsed Census data objects
*/
export function parseCensusResponse(
rawResponse: CensusApiResponse
): ReadonlyArray<CensusData> {
if (!rawResponse || rawResponse.length === 0) {
return [];
}
// First row contains headers: ["NAME", "P1_001N", "state"]
const headers = rawResponse[0];
const dataRows = rawResponse.slice(1);
// Find column indices
const nameIndex = headers.findIndex((h) => h === 'NAME');
const populationIndex = headers.findIndex((h) => h === 'P1_001N');
const stateIndex = headers.findIndex((h) => h === 'state');
if (nameIndex === -1 || populationIndex === -1 || stateIndex === -1) {
throw new Error('Invalid Census API response: missing required columns');
}
return dataRows.map((row) => ({
stateName: row[nameIndex],
population: parseInt(row[populationIndex], 10),
stateCode: row[stateIndex],
}));
}
/**
* Format Census data for human-readable output
*
* @param censusData - Structured Census data
* @param rawResponse - Optional raw response for header formatting
* @returns Formatted text output
*/
export function formatCensusData(
censusData: ReadonlyArray<CensusData>,
rawResponse?: CensusApiResponse
): string {
if (censusData.length === 0) {
return 'No census data available';
}
let output = 'Census Population Data:\n\n';
// Use headers from raw response if available, otherwise use defaults
if (rawResponse && rawResponse.length > 0) {
const headers = rawResponse[0];
output += `${headers.join('\t')}\n`;
output += '---\n';
// Use raw data rows for consistent formatting
const rows = rawResponse.slice(1);
for (const row of rows) {
output += `${row.join('\t')}\n`;
}
} else {
// Fallback formatting
output += 'State Name\tPopulation\tState Code\n';
output += '---\n';
for (const data of censusData) {
output += `${data.stateName}\t${data.population}\t${data.stateCode}\n`;
}
}
return output;
}
/**
* Build Census API query string for state codes
*
* @param states - Array of FIPS state codes ([0] means all states)
* @returns Query string for state parameter
*/
export function buildStateQuery(states: ReadonlyArray<number>): string {
if (!states || states.length === 0 || states[0] === 0) {
return '*';
}
return states.join(',');
}
/**
* Validate population request parameters
*
* @param request - Population request to validate
* @returns Validation result with error message if invalid
*/
export function validatePopulationRequest(request: PopulationRequest): {
valid: boolean;
error?: string;
} {
if (!request.states || !Array.isArray(request.states)) {
return {
valid: false,
error: 'states parameter must be an array',
};
}
if (request.states.length === 0) {
return {
valid: false,
error: 'states array cannot be empty',
};
}
for (const state of request.states) {
if (typeof state !== 'number') {
return {
valid: false,
error: 'all state codes must be numbers',
};
}
// Valid FIPS state codes are 0 (all) or 1-56
if (state !== 0 && (state < 1 || state > 56)) {
return {
valid: false,
error: `invalid FIPS state code: ${state}. Must be 0 (all) or 1-56`,
};
}
}
return { valid: true };
}
/**
* Create a success result
*
* @param data - Census data
* @param formattedOutput - Formatted text output
* @returns Success result
*/
export function createSuccessResult(
data: ReadonlyArray<CensusData>,
formattedOutput: string
): PopulationResult {
return {
success: true,
data,
formattedOutput,
};
}
/**
* Create an error result
*
* @param error - Error message
* @returns Error result
*/
export function createErrorResult(error: string): PopulationResult {
return {
success: false,
error,
};
}