[0, 1], 'A' => [1, 1], 'B' => [2, 1], 'C' => [3, 1], 'D' => [4, 1], 'E' => [5, 1], 'F' => [6, 1], 'G' => [7, 1], 'H' => [8, 1], 'I' => [9, 1], // Negative (EBCDIC / Standard ASCII) '}' => [0, -1], 'J' => [1, -1], 'K' => [2, -1], 'L' => [3, -1], 'M' => [4, -1], 'N' => [5, -1], 'O' => [6, -1], 'P' => [7, -1], 'Q' => [8, -1], 'R' => [9, -1], // Negative (GnuCOBOL / Micro Focus ASCII) 'p' => [0, -1], 'q' => [1, -1], 'r' => [2, -1], 's' => [3, -1], 't' => [4, -1], 'u' => [5, -1], 'v' => [6, -1], 'w' => [7, -1], 'x' => [8, -1], 'y' => [9, -1], ]; if (isset($map[$lastChar])) { [$lastDigit, $sign] = $map[$lastChar]; } elseif (ctype_digit($lastChar)) { $lastDigit = (int)$lastChar; $sign = 1; } else { $lastDigit = 0; $sign = 1; } $numStr = $digits . $lastDigit; return $sign * (int)$numStr; } /** * Formats integer cents (sen) to trailing zoned decimal S9(10)V99 (12 chars) * Positive values are plain digits, negative values overpunch the last digit to ASCII style (p-y) */ function formatZoned(int $sen, int $length = 12): string { $absVal = abs($sen); $str = str_pad((string)$absVal, $length, '0', STR_PAD_LEFT); if ($sen < 0) { $lastDigit = (int)$str[$length - 1]; $lastChar = chr(0x70 + $lastDigit); $str = substr($str, 0, $length - 1) . $lastChar; } return $str; } /** * Formats integer cents (sen) to COMP-3 packed decimal (7 bytes) * One pad nibble 0, 12 digit nibbles, and one sign nibble (0xC / 0xD) */ function formatComp3(int $sen, int $numDigits = 12): string { $absVal = abs($sen); $digitsStr = str_pad((string)$absVal, $numDigits, '0', STR_PAD_LEFT); $nibblesStr = '0' . $digitsStr; $signNibble = ($sen >= 0) ? 'c' : 'd'; $nibblesStr .= $signNibble; return pack('H*', $nibblesStr); } /** * Emulates COBDATFT behaviour with CODATECN-TYPE='2' and CODATECN-OUTTYPE='2' * Converts YYYY-MM-DD to YYYYMMDD left-justified in 20-char field, * truncated to 10-char output field. */ function convertDate(string $inpDate): string { if (strlen($inpDate) < 10) { return str_pad($inpDate, 10, ' ', STR_PAD_RIGHT); } $yyyy = substr($inpDate, 0, 4); $mm = substr($inpDate, 5, 2); $dd = substr($inpDate, 8, 2); return $yyyy . $mm . $dd . ' '; } // Execution main block function main(): void { $acctDataPath = getenv('ACCT_DATA'); $workDir = getenv('WORK'); if (!$acctDataPath || !$workDir) { fwrite(STDERR, "Error: ACCT_DATA or WORK environment variables are not set.\n"); exit(1); } if (!file_exists($acctDataPath)) { fwrite(STDERR, "Error: Input file does not exist at $acctDataPath\n"); exit(1); } if (!is_dir($workDir)) { if (!mkdir($workDir, 0777, true) && !is_dir($workDir)) { fwrite(STDERR, "Error: Work directory does not exist and could not be created at $workDir\n"); exit(1); } } $inHandle = fopen($acctDataPath, 'rb'); if (!$inHandle) { fwrite(STDERR, "Error: Could not open input file $acctDataPath\n"); exit(1); } $outPath = $workDir . '/outfile-gemini.dat'; $outHandle = fopen($outPath, 'wb'); if (!$outHandle) { fclose($inHandle); fwrite(STDERR, "Error: Could not open output file $outPath\n"); exit(1); } $recordsProcessed = 0; $bytesWritten = 0; // Persistent output record buffer state emulating COBOL memory behavior $out_record = [ 'OUT-ACCT-ID' => '', 'OUT-ACCT-ACTIVE-STATUS' => ' ', 'OUT-ACCT-CURR-BAL' => 0, 'OUT-ACCT-CREDIT-LIMIT' => 0, 'OUT-ACCT-CASH-CREDIT-LIMIT' => 0, 'OUT-ACCT-OPEN-DATE' => str_repeat(' ', 10), 'OUT-ACCT-EXPIRAION-DATE' => str_repeat(' ', 10), 'OUT-ACCT-REISSUE-DATE' => str_repeat(' ', 10), 'OUT-ACCT-CURR-CYC-CREDIT' => 0, 'OUT-ACCT-CURR-CYC-DEBIT' => 0, 'OUT-ACCT-GROUP-ID' => str_repeat(' ', 10), ]; while (($line = fgets($inHandle)) !== false) { $line = rtrim($line, "\r\n"); if (strlen($line) === 0) { continue; } if (strlen($line) < 300) { $line = str_pad($line, 300, ' ', STR_PAD_RIGHT); } // Parse input record (ACCOUNT-RECORD) using standard copybook layout $acct_id = trim(substr($line, 0, 11)); $acct_active_status = substr($line, 11, 1); $acct_curr_bal = parseZoned(substr($line, 12, 12)); $acct_credit_limit = parseZoned(substr($line, 24, 12)); $acct_cash_credit_limit = parseZoned(substr($line, 36, 12)); $acct_open_date = substr($line, 48, 10); $acct_expiraion_date = substr($line, 58, 10); $acct_reissue_date = substr($line, 68, 10); $acct_curr_cyc_credit = parseZoned(substr($line, 78, 12)); $acct_curr_cyc_debit = parseZoned(substr($line, 90, 12)); $acct_group_id = substr($line, 112, 10); // 1300-POPUL-ACCT-RECORD $out_record['OUT-ACCT-ID'] = $acct_id; $out_record['OUT-ACCT-ACTIVE-STATUS'] = $acct_active_status; $out_record['OUT-ACCT-CURR-BAL'] = $acct_curr_bal; $out_record['OUT-ACCT-CREDIT-LIMIT'] = $acct_credit_limit; $out_record['OUT-ACCT-CASH-CREDIT-LIMIT'] = $acct_cash_credit_limit; $out_record['OUT-ACCT-OPEN-DATE'] = $acct_open_date; $out_record['OUT-ACCT-EXPIRAION-DATE'] = $acct_expiraion_date; $out_record['OUT-ACCT-REISSUE-DATE'] = convertDate($acct_reissue_date); $out_record['OUT-ACCT-CURR-CYC-CREDIT'] = $acct_curr_cyc_credit; if ($acct_curr_cyc_debit === 0) { $out_record['OUT-ACCT-CURR-CYC-DEBIT'] = 252500; } else { $out_record['OUT-ACCT-CURR-CYC-DEBIT'] = $acct_curr_cyc_debit; } $out_record['OUT-ACCT-GROUP-ID'] = $acct_group_id; // 1350-WRITE-ACCT-RECORD $formatted_id = str_pad($out_record['OUT-ACCT-ID'], 11, '0', STR_PAD_LEFT); $formatted_status = str_pad($out_record['OUT-ACCT-ACTIVE-STATUS'], 1, ' ', STR_PAD_RIGHT); $formatted_curr_bal = formatZoned($out_record['OUT-ACCT-CURR-BAL'], 12); $formatted_credit_limit = formatZoned($out_record['OUT-ACCT-CREDIT-LIMIT'], 12); $formatted_cash_credit_limit = formatZoned($out_record['OUT-ACCT-CASH-CREDIT-LIMIT'], 12); $formatted_open_date = str_pad($out_record['OUT-ACCT-OPEN-DATE'], 10, ' ', STR_PAD_RIGHT); $formatted_expiraion_date = str_pad($out_record['OUT-ACCT-EXPIRAION-DATE'], 10, ' ', STR_PAD_RIGHT); $formatted_reissue_date = str_pad($out_record['OUT-ACCT-REISSUE-DATE'], 10, ' ', STR_PAD_RIGHT); $formatted_curr_cyc_credit = formatZoned($out_record['OUT-ACCT-CURR-CYC-CREDIT'], 12); $formatted_curr_cyc_debit = formatComp3($out_record['OUT-ACCT-CURR-CYC-DEBIT'], 12); $formatted_group_id = str_pad($out_record['OUT-ACCT-GROUP-ID'], 10, ' ', STR_PAD_RIGHT); $recordBuffer = $formatted_id . $formatted_status . $formatted_curr_bal . $formatted_credit_limit . $formatted_cash_credit_limit . $formatted_open_date . $formatted_expiraion_date . $formatted_reissue_date . $formatted_curr_cyc_credit . $formatted_curr_cyc_debit . $formatted_group_id; fwrite($outHandle, $recordBuffer); $recordsProcessed++; $bytesWritten += strlen($recordBuffer); } fclose($inHandle); fclose($outHandle); echo "Processed $recordsProcessed records, wrote $bytesWritten bytes.\n"; } main();