home *** CD-ROM | disk | FTP | other *** search
Wrap
Text File | 1990-06-15 | 54.6 KB | 2,107 lines
Newsgroups: comp.sources.misc subject: v13i056: combine: 3 file compare/merge utility (part 3of3) from: cliff@gcx1.SSD.CSD.HARRIS.COM (Cliff Van Dyke) Sender: allbery@uunet.UU.NET (Brandon S. Allbery - comp.sources.misc) Posting-number: Volume 13, Issue 56 Submitted-by: cliff@gcx1.SSD.CSD.HARRIS.COM (Cliff Van Dyke) Archive-name: combine/part03 # This is a shell archive. Remove anything before this line, # then unpack it by saving it in a file and typing "sh file". # # Wrapped by cliff on Tue Feb 13 14:14:08 EST 1990 # Contents: pass1.c pass3.c pass4.c pass5.c echo x - pass1.c sed 's/^@//' > "pass1.c" <<'@//E*O*F pass1.c//' /* * The combine utility is a product of Harris, Inc. and is provided for * unrestricted use provided that this legend is included on all tape * media and as a part of the software program in whole or part. Users * may copy, modify, license or distribute the combine utility without charge. * * THE COMBINE UTILITY IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND * INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE * PRACTICE. * * The combine utility is provided with no support and without any obligation * on the part of Harris, Inc. to assist in its use, correction, * modification or enhancement. * * HARRIS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THE COMBINE * UTILITY OR ANY PART THEREOF. * * In no event will Harris, Inc. be liable for any lost revenue * or profits or other special, indirect and consequential damages, even if * Harris has been advised of the possibility of such damages. * * Harris Computer Systems Division * 2101 W Cypress Creek Rd * Fort Lauderdale, Florida 33309 */ #include <stdio.h> #include <ctype.h> #include "util.h" #include "combine.h" /* * pass1: Perform the file read and table build pass * * This routine reads all of the records of all of the input files * into the cache, symbol table, and record array. * * This routine alternately reads one record from each file. Assuming * that the files have similar contents, this technique improves the * chance that a record from the second and third files will be in * the cache. * * Return value: * This procedure has no return value. */ void pass1 () { int i; /* Misc. variable */ int index; /* Index into record array */ int files_left; /* number of files left to do */ bool file_done[MAX_FILE_COUNT];/* TRUE if EOT reached on file */ int status; /* Misc. status variable */ /* * Initialize completion parameters. */ files_left = file_count; for (i = 0; i < file_count; ++i) { file_done[i] = FALSE; } /* * For each iteration of the file read the nth record of the each file. */ for (index = BEGIN_INDEX + 1; files_left != 0; ++index) { /* * Handle each file. */ for (i = 0; i < file_count; ++i) { if (!file_done[i]) { if (index + 1 >= files[i].record_array_alloc) { files[i].record_array_alloc += RA_INCR; files[i].record = (record_type *) realloc(files[i].record, files[i].record_array_alloc * sizeof (record_type)); if ( files[i].record == 0 ){ error( "out of memory -- files are way too big"); } } status = pass1_read_record (&files[i], index); if (status < 0) { file_done[i] = TRUE; files[i].record_array_size = index + 1; --files_left; if (files_left == 0) { break; } } } } } } /* * pass1_read_record: read a record for pass 1 * * This routine reads a record into the cache, symbol table, and record * array. * For each record read, a unique hash value is computed. * That hash value is an index into the symbol table. * * Return value: * 0: record read as requested. * -1: end of the file was reached. */ int pass1_read_record (file_ptr, index) file_type * file_ptr; /* input -- Description of the file to read record from. */ int index; /* input -- Record number of the record currently being read. */ { static cache_entry_type * cache_ptr = 0; /* Pointer to the cache entry which is used to read into. This cache buffer is linked onto the cache list if this is the first time the record is read. */ register int *end_int_ptr; /* Pointer to last integer in hashing algorithm */ cache_entry_type * found_cache_ptr; /* Pointer to the cache entry found by hashing */ register int *found_int_ptr;/* co-erced pointer to found record */ int hash_code; /* index into symbol table */ int i; /* Misc. variable */ register int *int_ptr;/* Co-erced pointer to read record */ char mbuffer[LINE_LENGTH];/* Misc. buffer */ int probe_count; /* Number a rehashes attempted. */ int quotient; /* quotient of sum/table size. This value is used as the increment when re-hashing into the symbol table */ cache_entry_type * reread_cache_ptr; /* Pointer to the cache buffer used for cache re-read operation. */ file_type * reread_file_ptr; /* Pointer to the file to read from on a cache re-read operation. */ int reread_index; /* Record number to read on a cache re-read operation. */ rfa_type rfa; /* RFA of record read */ int status; /* Misc. status variable */ register int sum; /* Sum of all bytes in a record */ /* * If we don't have a cache buffer laying around, * delink one from end of list. */ if (cache_ptr == 0) { deq_tail_dll (cache_head_ptr, cache_tail_ptr, cache_ptr, cache_next_ptr, cache_prev_ptr); if (cache_ptr -> hash_code != HASH_FREE_ENTRY) { sym_tab_cache_ptr[cache_ptr -> hash_code] = (cache_entry_type *) CACHE_NOT_IN_CACHE; } } /* * Read the next record into our local cache entry. */ rfa = ftell(file_ptr->seq_fd); if (p1_debug) { printf ("index: %d hash_col: %d cache_miss: %d", index, hash_collisions, cache_miss); } status = read_into_cache( file_ptr -> seq_fd, rfa, cache_ptr ); if (status < 0) { return (-1); } /* * Pad to end of word with blanks, this allows hashing and comparison * algorithms to be word oriented rather than byte oriented. * * Compress the record based on input parameters. */ if (cache_ptr -> record_length >= 0) { if (compress_records) { pass1_record_compress (cache_ptr -> recordp, &cache_ptr -> record_length); } /* Pad to the end of the word with blanks */ for (i = 0; i < sizeof (int) - 1; ++i) { cache_ptr->recordp[cache_ptr->record_length + i] = ' '; } } /* * Compute hash code for the record. * * The value is shifted on each iteration to ensure that the * magnitude of the sum is large enough. Otherwise when the sum is * divided by the size of the symbol table, the remainder will be * clustered toward the beginning of the table. */ sum = 0; int_ptr = (int *) cache_ptr -> recordp; end_int_ptr = int_ptr + ((cache_ptr->record_length + sizeof (int) - 1) / sizeof (int)); for (; int_ptr < end_int_ptr; int_ptr++) { /* if (p1_debug) { printf ("sum: %x ", sum); }*/ /* * The next statement does a left rotate of 7. The add operation is * used instead of a logical-or operation since the right shift * is an arithmetic shift which propogates the sign bit. An * or operation into 25 bits of propogated sign bit loses a lot * of significance. */ sum = (sum >> (sizeof (int) * 8 - 7)) + (sum << 7);/* left rotate 7 */ /* if (p1_debug) { printf ("sum<<7: %x ", sum); } */ sum += *int_ptr; /* if (p1_debug) { printf ("sum:%x value:%X\n", sum, *int_ptr); } */ } sum = abs (sum); if (p1_debug) { printf ("sum: %d ", sum); } hash_code = sum % sym_tab_size; quotient = sum / sym_tab_size; if ((quotient % sym_tab_size) == 0) { quotient = 1; } /* * Loop until a unique hash code is found. */ for (probe_count = 0; probe_count < sym_tab_size; ++probe_count, ++hash_collisions) { do { hash_code = (hash_code + quotient) % sym_tab_size; } while (hash_code == 0);/* ensure sym tab loc 0 is never used */ /* * if symbol table entry is used but record is not is cache, * read record into cache by flushing the least recently * entry entry from the cache. */ if (p1_debug) { printf ("hash_code: %d\n", hash_code); } if ((int) sym_tab_cache_ptr[hash_code] == CACHE_NOT_IN_CACHE) { cache_miss++; if (p1_debug) { printf ("cache_miss\n"); } deq_tail_dll (cache_head_ptr, cache_tail_ptr, reread_cache_ptr, cache_next_ptr, cache_prev_ptr); if (reread_cache_ptr -> hash_code != HASH_FREE_ENTRY) { sym_tab_cache_ptr[reread_cache_ptr -> hash_code] = (cache_entry_type *) CACHE_NOT_IN_CACHE; } for (i = 0; i < file_count; ++i) { if (files[i].sym_tab_index[hash_code] != 0) { reread_file_ptr = &files[i]; break; } } if (i == file_count) { error ("Consistency error on rehash read"); } reread_index = abs (reread_file_ptr -> sym_tab_index[hash_code]); reread_into_cache( reread_file_ptr, reread_index, reread_cache_ptr); if (reread_cache_ptr -> record_length >= 0) { if (compress_records) { pass1_record_compress ( reread_cache_ptr -> recordp, &reread_cache_ptr -> record_length); } for (i = 0; i < sizeof (int) - 1; ++i) { reread_cache_ptr-> recordp[reread_cache_ptr->record_length + i] = ' '; } } if (p1_debug) { printf ("%d %s\n", reread_cache_ptr -> record_length, reread_cache_ptr -> recordp); } sym_tab_cache_ptr[hash_code] = reread_cache_ptr; reread_cache_ptr -> hash_code = hash_code; enq_head_dll (cache_head_ptr, cache_tail_ptr, reread_cache_ptr, cache_next_ptr, cache_prev_ptr); } /* * if this is the first time this hash code has been used, * point the symbol table to the cache entry, * insert the cache entry in the front of the cache. * break out of loop. */ if (sym_tab_cache_ptr[hash_code] == CACHE_FREE_ENTRY) { if (p1_debug) { printf ("cache_free_entry\n"); } sym_tab_cache_ptr[hash_code] = cache_ptr; cache_ptr -> hash_code = hash_code; enq_head_dll (cache_head_ptr, cache_tail_ptr, cache_ptr, cache_next_ptr, cache_prev_ptr); cache_ptr = 0; break; /* * if this hash code is used and the record is in cache, * Compare our record with the one in the cache. * if the records are the same, * move cache entry to front of list, * break out of the loop. */ } else { found_cache_ptr = sym_tab_cache_ptr[hash_code]; if (p1_debug) { printf ("len1: %d len2: %d\n", cache_ptr -> record_length, found_cache_ptr -> record_length); } if (cache_ptr->record_length == found_cache_ptr->record_length) { found_int_ptr = (int *) found_cache_ptr->recordp; int_ptr = (int *) cache_ptr -> recordp; end_int_ptr = int_ptr + ((cache_ptr -> record_length + sizeof (int) - 1) / sizeof (int)); for (; int_ptr < end_int_ptr; int_ptr++) { /* printf( "v1: %x v2:%x\n", *int_ptr, *found_int_ptr ) ; */ if (*int_ptr != *found_int_ptr) { break; } found_int_ptr++; } if (int_ptr >= end_int_ptr) { rem_dll (cache_head_ptr, cache_tail_ptr, found_cache_ptr, cache_next_ptr, cache_prev_ptr); enq_head_dll (cache_head_ptr, cache_tail_ptr, found_cache_ptr, cache_next_ptr, cache_prev_ptr); break; } } } } if (probe_count == sym_tab_size) { error ("Symbol table overflow"); } /* * If the record array for the file has overflowed, * fatal error. */ if (index + 1 >= file_ptr -> record_array_alloc) { error ("record array size computed wrong"); } /* * Install record into record array. */ file_ptr -> record[index].rfa = rfa; file_ptr -> record[index].value[0] = -hash_code; file_ptr -> record[index].value[1] = -hash_code; /* * Install line into symbol table. */ if (file_ptr -> sym_tab_index[hash_code] == 0) { file_ptr -> sym_tab_index[hash_code] = index; } else { file_ptr -> sym_tab_index[hash_code] = -index; } return (0); } /* * pass1_record_compress: compress out insignificant bytes in a record. * * This routine handles options which compares only a portion of the * record. For the -b option, all occurrences of multiple blanks are * compressed into a single blank. For the -c option, all columns not * specified are removed. * * Return value: * This procedure has no return value. */ void pass1_record_compress (record_ptr, record_len_ptr) char *record_ptr; /* input/output */ /* Record to be compressed in place. */ int *record_len_ptr; /* input/output */ /* Length of record. */ { int i; /* Misc. variable */ int j; /* Misc. variable */ int k; /* Misc. variable */ bool space; /* TRUE if currently doing a space or tab sequence */ /* * Handle column ranges: */ if (column_count != 0) { k = 0; for (i = 0; i < column_count; ++i) { for (j = first_column[i]; j <= last_column[i] && j < *record_len_ptr; ++j) { record_ptr[k] = record_ptr[j]; ++k; } } *record_len_ptr = k; } /* * Handle blank compression: * * Blank compression converts all groups of blanks and horizontal * tabs to a single blank. */ if (blank_compress) { k = 0; space = FALSE; for (i = 0; i < *record_len_ptr; ++i) { if (record_ptr[i] == ' ' || record_ptr[i] == '\t') { space = TRUE; } else { if (space) { record_ptr[k++] = ' '; space = FALSE; } record_ptr[k++] = record_ptr[i]; } } if (space) { record_ptr[k++] = ' '; } *record_len_ptr = k; } /* * Handle blank removal: * * Blank removal removes all blanks and horizontal tabs. */ if (blank_remove) { k = 0; space = FALSE; for (i = 0; i < *record_len_ptr; ++i) { if (record_ptr[i] == ' ' || record_ptr[i] == '\t') { space = TRUE; } else { if (space) { space = FALSE; } record_ptr[k++] = record_ptr[i]; } } *record_len_ptr = k; } } @//E*O*F pass1.c// chmod u=rw,g=rw,o=rw pass1.c echo x - pass3.c sed 's/^@//' > "pass3.c" <<'@//E*O*F pass3.c//' /* * The combine utility is a product of Harris, Inc. and is provided for * unrestricted use provided that this legend is included on all tape * media and as a part of the software program in whole or part. Users * may copy, modify, license or distribute the combine utility without charge. * * THE COMBINE UTILITY IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND * INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE * PRACTICE. * * The combine utility is provided with no support and without any obligation * on the part of Harris, Inc. to assist in its use, correction, * modification or enhancement. * * HARRIS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THE COMBINE * UTILITY OR ANY PART THEREOF. * * In no event will Harris, Inc. be liable for any lost revenue * or profits or other special, indirect and consequential damages, even if * Harris has been advised of the possibility of such damages. * * Harris Computer Systems Division * 2101 W Cypress Creek Rd * Fort Lauderdale, Florida 33309 */ #include <stdio.h> #include <ctype.h> #include "util.h" #include "combine.h" /* * pass3: Expand anchors to non-unique records. * * If in a pair of files immediately adjacent to an anchor point * there are lines which are identical to each other, these lines * are then considered to be anchor points. Repeated application of this * principle on each possible pair of files and in each possible direction * results in all matched lines being found. * * This routine calls the 'pass3_scan' routine once for each combination of * file pairs and directions. * * Possible design flaw: If a particular anchor exists in all three files, * should adjacent records be considered to be an anchor only if all * three adjacent records are identical. * * Return value: * This procedure has no return value. */ void pass3 () { int i; /* Misc. variable */ int j; /* Misc. variable */ /* * Scan each pair of files in the forward direction. */ for (j = 0; j < UNIQUE_MATCH_COUNT; ++j) { i = unique_match[j]; if (files[curr_file[i]].record != 0 && files[corres_file[i]].record != 0) { pass3_scan (i, 1); } } /* * Scan each pair of files in the reverse direction. */ for (j = 0; j < UNIQUE_MATCH_COUNT; ++j) { i = unique_match[j]; if (files[curr_file[i]].record != 0 && files[corres_file[i]].record != 0) { pass3_scan (i, -1); } } } /* * pass3_scan: Expand anchors to non-unique records. * * This routine scans the record arrays for a pair of files in one * direction expanding anchors. For each record in the first file, * if the current record is an anchor, and the next record in each file * is a hash code (i.e. not an anchor), and the hash codes are the same, * then the next records are considered to be anchors. * * Return value: * This procedure has no return value. */ void pass3_scan (match_no, direction) int match_no; /* input */ /* Which relationship is being scanned */ int direction; /* This is the direction to scan the file. Valid values are 1 for forward and -1 for backward. */ { int end_index1; /* Index into record array of file1 of end of the record array. (e.g. the first record to not process when going in 'direction') */ file_type * file1_ptr; /* First file - current_file */ file_type * file2_ptr; /* Second file - corresponding file */ int file1_sub; /* For each record of the first file, this is a subscript of the 'value' array of the relationship between file1 and file2 */ int file2_sub; /* For each record of the second file, this is a subscript of the 'value' array of the relationship between file2 and file1 */ int index1; /* Index into record array of file1 of the record currently being processed */ int index2; /* Index into record array of file2 of the record anchored to the 'index1' record */ record_type * record1; /* Pointer to record array of file1 */ record_type * record2; /* Pointer to record array of file2 */ int *val1_ptr; /* Pointer to the 'value' field in 'next' record on file1. This is the 'value' which indicates the relationship to file2. */ int *val2_ptr; /* Pointer to the 'value' field in 'next' record on file2. This is the 'value' which indicates the relationship to file1. */ /* * Initialize indexes to first and last record of the files. */ file1_ptr = &files[curr_file[match_no]]; file2_ptr = &files[corres_file[match_no]]; file1_sub = value_sub[match_no]; file2_sub = rev_value_sub[match_no]; record1 = file1_ptr -> record; record2 = file2_ptr -> record; if (direction == 1) { index1 = BEGIN_INDEX; end_index1 = file1_ptr -> record_array_size - 1; } else { index1 = file1_ptr -> record_array_size - 1; end_index1 = BEGIN_INDEX; } /* * For each record in the first file. * * Note: given the existence of the 'begin' and 'end' record on each * end of the record array, and given the way the 'end_index1' * was set up above (so as to not process the last record * on the opposite end of the file from which the scan started), * we can convince ourselves that the tests below won't ever index * off the end of the arrays. */ for (; index1 != end_index1; index1 += direction) { /* * If the current record in file1 is an anchor point, * Compute the index into file2 of the corresponding record. * Compute a pointer to value field of the next record in each file. */ index2 = record1[index1].value[file1_sub]; if (!is_hash_code (index2)) { val1_ptr = &(record1[index1 + direction]. value[file1_sub]); val2_ptr = &(record2[index2 + direction]. value[file2_sub]); /* * If the neither of the next records are anchor points and * the records are identical, * consider these records to be anchors. */ if (is_hash_code (*val1_ptr) && is_hash_code (*val2_ptr) && *val1_ptr == *val2_ptr) { link_records (match_no, index1 + direction, index2 + direction); } } } } @//E*O*F pass3.c// chmod u=rw,g=rw,o=rw pass3.c echo x - pass4.c sed 's/^@//' > "pass4.c" <<'@//E*O*F pass4.c//' /* * The combine utility is a product of Harris, Inc. and is provided for * unrestricted use provided that this legend is included on all tape * media and as a part of the software program in whole or part. Users * may copy, modify, license or distribute the combine utility without charge. * * THE COMBINE UTILITY IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND * INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE * PRACTICE. * * The combine utility is provided with no support and without any obligation * on the part of Harris, Inc. to assist in its use, correction, * modification or enhancement. * * HARRIS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THE COMBINE * UTILITY OR ANY PART THEREOF. * * In no event will Harris, Inc. be liable for any lost revenue * or profits or other special, indirect and consequential damages, even if * Harris has been advised of the possibility of such damages. * * Harris Computer Systems Division * 2101 W Cypress Creek Rd * Fort Lauderdale, Florida 33309 */ #include <stdio.h> #include <ctype.h> #include "util.h" #include "combine.h" /* * pass4: Handle non-unique record clusters. * * This routine tries to fix up problems which arise due to groups of * non-unique records which are surrounded by insertions. When this * condition arises, the group of non-unique records are treated as * a part of the inserted text. * * This routine finds a pair of anchor records which have inserted lines * between them. (See diagram below.) All inserted lines are compared * to one another trying to find a group of non-unique lines which * match. Such lines are considered equal and are flagged as anchor * points. * * match1 match1 * insert * non-unique1 non-unique1 * insert * match2 match2 * * This routine runs in M x N time where 'M' is the number of lines * inserted at the current position in the first file and 'N' is the number * of lines inserted at the current position in the second file. * * Possible design flaw: The algorithm used to match the non-unique lines * should really be the same alogirthm used for the rest of the file. * That algorithm runs in linear time. The main problem with doing that is * that the other passes of this program were not designed for or optimized * around working for a portion of a file. Also, the proposed algorithm * relies on the fact that some of the non-unique lines will become unique * when only this fragment of the files are compared. * * Return value: * This procedure has no return value. */ void pass4 () { int i; /* Misc. variable */ int j; /* Misc. variable */ /* * Scan each pair of files. */ for (j = 0; j < UNIQUE_MATCH_COUNT; ++j) { i = unique_match[j]; if (files[curr_file[i]].record != 0 && files[corres_file[i]].record != 0) { pass4_scan (i); } } } /* * pass4_scan: pass4 for two files. * * This routine implements the pass4 algorithm for a pair of files. * * Return value: * This procedure has no return value. */ void pass4_scan (match_no) int match_no; /* input */ /* Which relationship is being scanned */ { register int i; /* Index into record array of file1 of the current position in the inserted records */ register int j; /* Index into record array of file2 of the current position in the inserted records */ file_type * file1_ptr; /* First file - current_file */ file_type * file2_ptr; /* Second file - corresponding file */ int file1_sub; /* For each record of the first file, this is a subscript of the 'value' array of the relationship between file1 and file2 */ int file2_sub; /* For each record of the second file, this is a subscript of the 'value' array of the relationship between file2 and file1 */ register int fn_index1; /* Index into record array of file1 of the position of the last inserted record */ register int fn_index2; /* Index into record array of file2 of the position of the last inserted record */ int index1; /* Index into record array of file1 of the record currently being processed */ record_type * record1; /* Pointer to record array of file1 */ record_type * record2; /* Pointer to record array of file2 */ register int st_index1; /* Index into record array of file1 of the position of the first inserted record */ register int st_index2; /* Index into record array of file2 of the position of the first inserted record */ /* * Initialize indexes to first and last record of the files. */ file1_ptr = &files[curr_file[match_no]]; file2_ptr = &files[corres_file[match_no]]; file1_sub = value_sub[match_no]; file2_sub = rev_value_sub[match_no]; record1 = file1_ptr -> record; record2 = file2_ptr -> record; /* * For each record in the first file. * * Note: given the existence of the 'begin' and 'end' record on each * end of the record array, * we can convince ourselves that the tests below won't ever index * off the end of the arrays. */ for (index1 = BEGIN_INDEX; index1 < file1_ptr->record_array_size-1; index1++) { /* * If the current record in file1 is not an anchor point, * continue the big loop. */ st_index2 = record1[index1].value[file1_sub]; if (is_hash_code (st_index2)) { continue; } /* * Compute the index into file2 of the corresponding record. * Compute a pointer to value field of the next record in each * file. * * If either of the next records are anchor points, * continue the big loop. */ if (!is_hash_code( record1[index1 + 1].value[file1_sub])) continue; if (!is_hash_code( record2[st_index2 + 1].value[file2_sub])) continue; /* * We have found the point where a group of inserted records * begins in both files. * * Set up indexes to the first and last inserted records in * the group. */ st_index1 = fn_index1 = index1 + 1; st_index2 = st_index2 + 1; while (is_hash_code (record1[fn_index1].value[file1_sub])) { ++fn_index1; } fn_index2 = record1[fn_index1].value[file1_sub]; fn_index1--; /* Adjust to index to last inserted record */ fn_index2--; /* Adjust to index to last inserted record */ /* * If this group of inserted records is adjacent to a group of * moved records, skip the comparison. This isn't the case * we're interested in. */ index1 = fn_index1; if (st_index2 > fn_index2) { continue; } /* * Ensure that only non-unique records exist in the range * of the corresponding file. This catches a move in the * other direction. */ for ( j=st_index2; j<=fn_index2; ++ j){ if (!is_hash_code (record2[j].value[file2_sub])) break; } if ( j <= fn_index2 ) continue; /* * Compare the hash codes. If two records are identical, * consider them to be anchor points. */ if ( p4_debug ) { printf("pass4: match_no: %d (%d-%d) (%d-%d)\n", match_no, st_index1, fn_index1, st_index2, fn_index2); } for (i = st_index1; i<=fn_index1; ++i) { for (j=st_index2; j<=fn_index2; ++j) { if (record1[i].value[file1_sub] == record2[j].value[file2_sub]) { link_records(match_no,i, j); st_index2 = j + 1; break; } } } } } @//E*O*F pass4.c// chmod u=rw,g=rw,o=rw pass4.c echo x - pass5.c sed 's/^@//' > "pass5.c" <<'@//E*O*F pass5.c//' /* * The combine utility is a product of Harris, Inc. and is provided for * unrestricted use provided that this legend is included on all tape * media and as a part of the software program in whole or part. Users * may copy, modify, license or distribute the combine utility without charge. * * THE COMBINE UTILITY IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND * INCLUDING THE WARRANTIES OF DESIGN, MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE * PRACTICE. * * The combine utility is provided with no support and without any obligation * on the part of Harris, Inc. to assist in its use, correction, * modification or enhancement. * * HARRIS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THE COMBINE * UTILITY OR ANY PART THEREOF. * * In no event will Harris, Inc. be liable for any lost revenue * or profits or other special, indirect and consequential damages, even if * Harris has been advised of the possibility of such damages. * * Harris Computer Systems Division * 2101 W Cypress Creek Rd * Fort Lauderdale, Florida 33309 */ #include <stdio.h> #include <ctype.h> #include "util.h" #include "combine.h" /* * pass5: Output compared records * * The record arrays for the three files are traversed outputing the * resultant file. An index is maintained into each of the record arrays. * As each index is incremented the record is output. This routine merely * determines which file the next record is to come from. * * Return value: * This procedure has no return value. */ void pass5 () { int i; /* Misc. variable */ int indexes[MAX_FILE_COUNT]; /* index into each of the record arrays */ int j; /* Misc. variable */ record_type * new_rec_ptr;/* Pointer to the current record */ record_type * old_rec_ptr;/* Pointer to the record for the previously current record */ relate_type relate; /* Relationships for the current record under consideration */ /* * Initialize the scan. * * Note: for files which don't exist, the value initialized below * needs to yield a TRUE result for the end of file comparison done * below. */ for (i = 0; i < MAX_FILE_COUNT; ++i) { indexes[i] = BEGIN_INDEX + 1;/* skip the dummy begin record */ } /* * For each line output. */ for (;;) { /* * For each file, * if the current record in the file is not involved in a * move operation, then the record should be dumped * immediately. * * This condition occurs for the following cirucumstances: * The record exists in the current position in all * three files. * The record exists in the current position in this * file and one of the other files. However, * the record does not occur anywhere in the * third file. * The record exists in the current position in this * file and does not occur anywhere in the other * two files. */ for (i = 0; i < file_count; ++i) { pass5_analyze_relationship ( i,/* file containing record */ indexes, /* current posit. in all files */ &relate); /* Returns file relationships */ if (!relate.moved) { if (indexes[OLD_FILE] >= files[OLD_FILE].record_array_size - 1 && indexes[NEW1_FILE] >= files[NEW1_FILE].record_array_size-1 && indexes[NEW2_FILE] >= files[NEW2_FILE].record_array_size-1) { relate.relation = INSERT_EOT; pass5_dump_record (i, indexes, &relate); return; } pass5_dump_record (i, indexes, &relate); goto continue_big_loop; } } /* * All other cases involve a move of text from one location to * another. * * The 'pass5_move' routine analyses which text is the shortest text. */ i = pass5_move (indexes); pass5_analyze_relationship ( i, /* file containing record */ indexes, /* current position in all files */ &relate); /* Returns file relationships */ old_rec_ptr = &(files[i].record[indexes[i]]); for (;;) { pass5_dump_record (i, indexes, &relate); /* * If any of the files involved in the move have now * reached EOT, * go on to process bigger and better things. */ for (j = 0; j < file_count; ++j) { if (!is_hash_code (relate.index[j])) { relate.index[j]++; if (relate.index[j] + 1 >= files[j].record_array_size) { goto continue_big_loop; } } } /* * Note: this code should continue to dump records * without re-iterating the pass5_move routine. * Since the pass5_move routine executes in a loop, * the above_mentioned enhancement is required to ensure * that pass5 completes in linear time. * * The test below determines if the current record * is logically next to the previous record. * If so, the record is merely dumped. * Note: the record relationship does not change * for this record. * * A record is logically next to a previous one * if either the value field is a hash code for * both records or if the record index in the * value field is one greater then that of the * previous record. */ new_rec_ptr = &(files[i].record[indexes[i]]); for (j = 0; j < MAX_VALUE_SUB; ++j) { if (is_hash_code (old_rec_ptr -> value[j])) { if(is_hash_code(new_rec_ptr->value[j])){ continue; } } else if (old_rec_ptr->value[j]+1 == new_rec_ptr -> value[j]) { continue; } goto continue_big_loop; } old_rec_ptr = new_rec_ptr; } continue_big_loop: ; } } /* * pass5_analyze_relationship -- determine relationsip to other files * * This routine determines the relationship between one record in one file * and the records at the current position in the other files. * The routine returns information regarding whether the record exists * in each file at the current position and whether the record is involved * in a move operation. * * Return value: * This procedure returns no value. */ void pass5_analyze_relationship (file_no, indexes, rel_ptr) int file_no; /* input -- file number of the file containing the record to be compared. */ int indexes[MAX_FILE_COUNT];/* input -- Array of the current indexes into the record arrays */ relate_type * rel_ptr; /* output -- Returns relationship between current record and records at the current position in the corresponding files. */ { int i; /* Misc. variable */ int power; /* Current power of 2 */ record_type * record_ptr;/* Pointer to the record block is the record currently being analyzed. */ /* * Initialize the value_index array to contain the index of the current * record in each of the files. * * The record is at the current position in the current file. The record * is in the position indicated by the 'value' field in the record entry * for the other files. */ record_ptr = &(files[file_no].record[indexes[file_no]]); rel_ptr -> index[file_no] = indexes[file_no]; rel_ptr -> index[corres_file[file_no * 2]] = record_ptr -> value[value_sub[file_no * 2]]; rel_ptr -> index[corres_file[file_no * 2 + 1]] = record_ptr -> value[value_sub[file_no * 2 + 1]]; /* * Compare the current position in each file with the index of * this record in each file. */ rel_ptr -> moved = FALSE; rel_ptr -> in_all = TRUE; rel_ptr -> relation = 0; power = 1; for (i = 0; i < file_count; ++i) { rel_ptr -> current[i] = (rel_ptr -> index[i] == indexes[i]); if (rel_ptr -> current[i]) { rel_ptr -> relation += power; } else { rel_ptr -> in_all = FALSE; if (!is_hash_code (rel_ptr -> index[i])) { rel_ptr -> moved = TRUE; } } power *= 2; } } /* * pass5_dump_record: write a record for pass 5 * * This routine writes the specified record to the output files and * increments the appropriate indexes. * * Return value: * This routine returns no value. */ void pass5_dump_record (file_no, indexes, rel_ptr) int file_no; /* input -- File number of the file to read record from. */ int indexes[MAX_FILE_COUNT];/* input/output -- Indexes of the current records. Each index is incremented as appropriate */ relate_type * rel_ptr; /* input -- Relationships of this record to other files. */ { cache_entry_type * cache_ptr;/* Current cache entry pointer */ file_type * file_ptr; /* Pointer to the current file */ int i; /* Misc. variable */ /* * Consider the case that the comparison was done with the -B * option. In that case, records reported to be identical here * may actually only be similar. The loop below computes the * largest file_no which has a record "identical" to the * current one. The theory is that the largest file_no contains * the "newest" record (and by deduction, the "best" record). */ for (i = file_no+1; i < file_count; ++i) { if (rel_ptr -> current[i]) { if ( p5_debug ) { printf ( "debug: oldfile:%d newfile: %d\n", file_no, i ); } file_no = i; } } /* * If this is the end of the world, * Pass the word on. */ if (rel_ptr -> relation == INSERT_EOT) { if (hed_flag) { pass5_write_hed (rel_ptr, (cache_entry_type *) 0); } else { pass5_write_listing(indexes, rel_ptr, (cache_entry_type *) 0); } return; } /* * Initialize local variables. * * The cache of records is maintained as a queue of the previously seen * records. This queue is used by the 'write' routines. */ file_ptr = &files[file_no]; deq_tail_dll (cache_head_ptr, cache_tail_ptr, cache_ptr, cache_next_ptr, cache_prev_ptr); enq_head_dll (cache_head_ptr, cache_tail_ptr, cache_ptr, cache_next_ptr, cache_prev_ptr); /* * Write record to output files. */ if (p5_debug) { printf ( "debug: file:%d indexes:%5d %5d %5d current: %d %d %d move: %d\n", file_no, indexes[0], indexes[1], indexes[2], rel_ptr -> current[0], rel_ptr -> current[1], rel_ptr -> current[2], rel_ptr -> moved); } if (hed_flag) { /* if hed_flag, always read record into cache */ reread_into_cache( file_ptr, indexes[file_no], cache_ptr ); pass5_write_hed (rel_ptr, cache_ptr); } else { /* for normal output, wait to read into cache until we know for sure it is going to be needed. Most records are merely skipped and not output at all. */ pass5_write_listing (indexes, rel_ptr, cache_ptr); } /* * Increment the count of differences. */ switch (rel_ptr -> relation) { case INSERT_OLD: case INSERT_NEW1_NEW2: old_new1_change_count++; old_new2_change_count++; break; case INSERT_NEW1: case INSERT_OLD_NEW2: old_new1_change_count++; new1_new2_change_count++; break; case INSERT_NEW2: case INSERT_OLD_NEW1: old_new2_change_count++; new1_new2_change_count++; break; case INSERT_OLD_NEW1_NEW2: break; default: error ("pass5_dump_record: invalid relation"); } /* * Increment the record indexes. */ for (i = 0; i < file_count; ++i) { if (rel_ptr -> current[i]) { indexes[i]++; } } } /* * pass5_move: Analyse which text moved. * * This routine is called when a group of moved lines is detected. * A group of moved lines exists when (for the current position in * the file) the next line occurs in all three files but at different * relative positions. This routine scans forward in the each of the * 3 files trying to determine the shortest group of lines. * * In determining the shortest group of lines, inserted and deleted records * are not considered. Rather, the scan continues until a line which is * before the current line is found. * * Return value: * This procedure return the file number of the file to be traversed * to indicate the shortest common sequence of lines. */ int pass5_move (indexes) int *indexes; /* Array of pointers to the current indexes into the record arrays */ { int corres_file_recno; /* Record number in the corresponding file */ int depth; /* Number of lines tested */ int i; /* Misc. variable */ record_type * record; /* Pointer to record array for the current file. */ /* * For each record, six different comparison need to be made. That is, * for each of the three files there is a relationship to each of the * other two files. A comparison needs to be made for each of those * relationships. The tables below describe the six relationships. */ int index[MATCH_COUNT];/* Current index into the record array of 'curr_file' */ int prev_value[MATCH_COUNT]; /* Previous contents of the 'value' field for the record */ bool bypass[MATCH_COUNT];/* TRUE if file should not by considered for processing */ /* * Initialize the index array and the prev_value array from the values * passed in. * * During this initialization, all entries which contain hash codes * are skipped over. All such entries represent a line which exists in * only two of the files. */ for (i = 0; i < MATCH_COUNT; ++i) { bypass[i] = (files[curr_file[i]].record == 0) || (files[corres_file[i]].record == 0) || (indexes[curr_file[i]] + 1 >= files[curr_file[i]].record_array_size); if (!bypass[i]) { record = files[curr_file[i]].record; index[i] = indexes[curr_file[i]]; while(is_hash_code( record[index[i]].value[value_sub[i]])) { index[i]++; } prev_value[i] = record[index[i]].value[value_sub[i]]; } } /* * Indentify the shortest common sequence of lines. * * For each iteration of this loop the index into the record array for * each of the six cases is incremented by one. The record detected at the * new index is compared against the previous record for the same case. * * In the documentation below, the 'current' file is the one which * has a relationship and the 'corresponding' file is the one related to. */ for (depth = 0;; depth++) { for (i = 0; i < MATCH_COUNT; ++i) { /* * If this files has no records or no records left * to dump, * ignore the file. * * This happens in the case of a two file comparision. * or in the case of one file completely output. */ if (bypass[i]) { continue; } /* * If we have reached the end of the 'current' file, * If this line is also the end of the * 'corresponding' file, * If the 'corresponding' file is at EOF already, * the 'current' file contains the * shortest sequence * else * the 'corresponding' file contains the * shortest sequence * else * the 'current' file contains the shortest * common sequence. */ if(index[i]+1 >= files[curr_file[i]].record_array_size){ if (prev_value[i] + 1 >= files[corres_file[i]].record_array_size) { if (indexes[corres_file[i]] + 1 >= files[corres_file[i]]. record_array_size) { return (curr_file[i]); } else { return (corres_file[i]); } } else { return (curr_file[i]); } } /* * For each record in the 'current' file which doesn't * have a record in the 'corresponding' file, * skip past the record. * * Since what we are trying to find is the shortest * group of records in the 'corresponding' file, * the search should not be penalized by * inserted records in the 'current' file. */ record = files[curr_file[i]].record; index[i]++; while (is_hash_code(record[index[i]]. value[value_sub[i]])) { index[i]++; } /* * If the record number in the corresponding file * is one of the records being considered as * moved by the 'corresponding' file, the 'current' * file contains the shortest common sequence. * * Note: this routine is only invoked if all files * are currently involved in a move. * * By outputting lines from the 'current' file, * we will soon get to the current point in the * 'corresponding' file. At that point, the * current line in the 'corresponding' file will * no longer be diagnosed as having moved. */ corres_file_recno = record[index[i]]. value[value_sub[i]]; if (corres_file_recno >= indexes[corres_file[i]] && corres_file_recno <= indexes[corres_file[i]] + depth) { if (p5_debug) printf("move optimization found\n"); return (curr_file[i]); } } /* * The above loop and this loop were split in order to give the * above loop every opportunity to find a move optimization. */ for (i = 0; i < MATCH_COUNT; ++i) { if (bypass[i]) continue; record = files[curr_file[i]].record; corres_file_recno = record[index[i]]. value[value_sub[i]]; /* * If the record numbers in the 'corresponding' file are * still going in the forward direction, * then we haven't completed the search. * * Record numbers need not be immediately next to * one another (e.g. record number 5 and record * number 6). Record numbers merely need to * following one another. This ensures that the common * sequence check isn't interrupted by deletions in the * 'corresponding' file. * * The above comment is hereby rescinded. Cliff 1/23/85 */ if (prev_value[i] + 1 == corres_file_recno) { prev_value[i] = corres_file_recno; } else { if (p5_debug) printf("short move found %d\n", depth); return (curr_file[i]); } } } } /* * pass5_write_hed: write a record to 'hed' file. * * This routine writes the specified record to the 'hed' output file. * * Return value: * This routine returns no value. */ void pass5_write_hed (rel_ptr, cache_ptr) relate_type * rel_ptr; /* input */ /* Relationships of this record to other files. */ cache_entry_type * cache_ptr; /* input */ /* Cache entry describing buffer */ { bool flag; /* TRUE if NEW1 and OLD file not same */ int i; /* Misc. variable */ #define HED_END_RECORD "~End of changes" static int prev_relation = INSERT_NONE; /* Relationship of previous call */ static bool prev_in_all = TRUE; /* TRUE if previous record was in all of the files */ /* * Handle EOT case. */ if (rel_ptr -> relation == INSERT_EOT) { if (!prev_in_all) { puts (HED_END_RECORD); } return; } /* * Write any header of trailing records to 'hed' file. */ if (rel_ptr -> relation != prev_relation) { if (!prev_in_all) { puts (HED_END_RECORD); } if (!rel_ptr -> in_all) { (void) fputs ("~~", stdout); if (rel_ptr -> current[OLD_FILE]) { (void) fputs ("Delete ", stdout); } else { (void) fputs ("Insert ", stdout); } flag = FALSE; for (i = NEW1_FILE; i < file_count; ++i) { if (rel_ptr -> current[OLD_FILE] != rel_ptr -> current[i]) { if (flag) { (void) fputs (" and ", stdout); } (void) fputs("'", stdout); (void) fputs(files[i].text_ptr? files[i].text_ptr: files[i].name_ptr, stdout); (void) fputs("'", stdout); flag = TRUE; if (!is_hash_code(rel_ptr->index[i]) && !is_hash_code( rel_ptr->index[OLD_FILE])) { (void) fputs(" (MOVED)",stdout); } } } (void) putchar ('\n'); } prev_relation = rel_ptr -> relation; prev_in_all = rel_ptr -> in_all; } /* * Write record to 'hed' file. */ cache_ptr -> recordp[cache_ptr -> record_length] = '\0'; (void) puts (cache_ptr -> recordp); } /* * pass5_write_listing: write a record to 'listing' file. * * This routine writes a summary of the changes to the standard output. * * This routine writes all changed lines to standard output. It also * writes a few (-prefix and -postifx options) unchanged lines immediately * preceeding and following the changed lines. This routine assumes that * the caller, 'pass5_dump_record', is maintaining a queue of all records * read in the cache. The current record should be at the head of the * cache. The previous record should immediately follow that, etc. * * Return value: * This routine returns no value. */ void pass5_write_listing (indexes, rel_ptr, cache_ptr) int indexes[MAX_FILE_COUNT];/* input */ /* Indexes of the current records. */ relate_type * rel_ptr; /* input */ /* Relationships of this record to other files. */ cache_entry_type * cache_ptr; /* input */ /* Cache entry describing buffer */ { static relate_type all_rel; /* relation used for outputting prefix lines.*/ int i; /* Misc. variable */ static int postfix_count = 0; /* Number of postfix lines remaining to output*/ cache_entry_type * prefix_cache_ptr; /* Pointer to cache entry containing the current prefix line */ int prefix_count; /* Number of prefix lines to output */ static int prev_relation = INSERT_NONE; /* Relationship of previous call */ static bool prev_in_all = TRUE; /* TRUE if previous line was in all files at the current position */ static int same_count = 0; /* Number of indentical lines scanned so far which might later be output as prefix lines. */ /* * Handle EOT case. */ if (rel_ptr -> relation == INSERT_EOT) { if (!quiet_option) { if (old_new1_change_count == 0 && (file_count == 2 || (old_new2_change_count == 0 && new1_new2_change_count == 0))) { (void) strcpy (cache_tail_ptr -> recordp, "Files are identical"); cache_tail_ptr -> record_length = strlen (cache_tail_ptr -> recordp); pass5_write_listing_line ((relate_type *) 0, cache_tail_ptr); } else { cache_tail_ptr -> record_length = 0; pass5_write_listing_line((relate_type *) 0, cache_tail_ptr); (void) strcpy(cache_tail_ptr->recordp, "[ Comparison Complete ]"); cache_tail_ptr->record_length = strlen(cache_tail_ptr -> recordp); pass5_write_listing_line((relate_type *) 0, cache_tail_ptr); } (void) putchar('\f'); } return; } /* * Write any prefix lines which need to be written. */ if (rel_ptr -> relation != prev_relation && prev_in_all) { prefix_count = min (prefix_lines, same_count); if (same_count > prefix_count) { /* Output seperator line */ pass5_write_listing_line ((relate_type *) 0, (cache_entry_type *) 0); } prefix_cache_ptr = cache_ptr; for (i = 0; i < prefix_count; ++i) { prefix_cache_ptr = prefix_cache_ptr -> cache_next_ptr; } all_rel.relation = INSERT_OLD_NEW1_NEW2; all_rel.moved = FALSE; all_rel.in_all = TRUE; while (prefix_count > 0) { for (i = 0; i < file_count; ++i) { all_rel.index[i] = indexes[i] - prefix_count; all_rel.current[i] = TRUE; } pass5_write_listing_line ( &all_rel, /* Indicate all lines are related */ prefix_cache_ptr); /* Pointer to record in cache */ prefix_count--; prefix_cache_ptr = prefix_cache_ptr -> cache_prev_ptr; } } /* * If this is the first identical line, * indicate how many postifx lines need to be output. */ if (rel_ptr -> relation != prev_relation && rel_ptr -> in_all && prev_relation != INSERT_NONE) { postfix_count = postfix_lines; } prev_relation = rel_ptr -> relation; prev_in_all = rel_ptr -> in_all; /* * If this line is a changed line or a postfix line, * dump it. */ if (!rel_ptr -> in_all || postfix_count > 0) { pass5_write_listing_line ( rel_ptr,/* Line relationships */ cache_ptr);/* Pointer to record in cache */ } /* * Count the number of identical lines * and keep track of the number of postfix lines printed. */ if (rel_ptr -> in_all) { if (postfix_count <= 0) { same_count++; } postfix_count--; } else { same_count = 0; } } /* * pass5_write_listing_line: write a record to 'listing' file. * * This routine writes a single line to the 'listing' file. The line has * already been checked to ensure that the line is to be output. * * Return value: * This routine returns no value. */ void pass5_write_listing_line (rel_ptr, cache_ptr) relate_type * rel_ptr; /* input -- Relationships of this record to other files. If this pointer is 0, write the line pointed to by cache_entry_type with no special formatting */ cache_entry_type * cache_ptr; /* input -- Cache entry describing buffer If this pointer is 0, write a seperator line between changes */ { int i; /* Misc. variable */ static int line_count = 0;/* Number of lines left on a page */ static int page_count = 0;/* Number of pages output so far */ /* * Output the header lines, if required. * * Don't output seperator record too close to bottom of a page. */ --line_count; if (line_count <= 0 || (line_count <= 10 && cache_ptr == 0)) { line_count = 50; if (page_count) (void) putchar('\f'); page_count++; printf ("Time of comparison: %s Page %d\n\n", exec_time, page_count); if (file_count == 2) { pass5_write_listing_head("Old ", OLD_FILE); pass5_write_listing_head("New ", NEW1_FILE); (void) putchar ('\n'); puts ("Status Old New Contents"); puts ("------ --- --- --------"); } else { pass5_write_listing_head("Old ", OLD_FILE); pass5_write_listing_head("New1", NEW1_FILE); pass5_write_listing_head("New2", NEW2_FILE); (void) putchar ('\n'); puts ("Status Old New1 New2 Contents"); puts ("------ --- ---- ---- --------"); } puts ("\n"); if (cache_ptr == 0) { return; /* Don't put seperator record at top of page */ } } /* * If rel_ptr exists, * Output the Line status and file line numbers: */ if (rel_ptr != 0) { if (!rel_ptr -> in_all) { if (rel_ptr -> current[OLD_FILE]) { if (rel_ptr -> moved) { fputs ("MOV[O]", stdout); } else { fputs ("Delete", stdout); } } else { if (rel_ptr -> moved) { fputs ("MOV[N]", stdout); } else { fputs ("Insert", stdout); } } } else { fputs (" ", stdout); } cache_ptr->record_length = -1; for (i = 0; i < file_count; ++i) { if (is_hash_code (rel_ptr -> index[i])) { fputs (" ", stdout); } else { if (cache_ptr->record_length == -1) { reread_into_cache( &files[i], rel_ptr->index[i], cache_ptr ); } printf ("%6d ", rel_ptr -> index[i]); } } } else if (cache_ptr == 0) { puts ("******"); return; } /* * Finally, write the line. */ if (cache_ptr -> record_length == -1) { puts (" < EOF.. >"); } else { cache_ptr -> recordp[cache_ptr -> record_length] = '\0'; puts (cache_ptr -> recordp); } } /* * pass5_write_listing_head: write a header line to 'listing' file. * * This routine writes a single header line to the 'listing' file. * * Return value: * This routine returns no value. */ void pass5_write_listing_head (name_ptr, file_no) char * name_ptr; /* input -- name of header */ int file_no; /* input -- file number of the file containing the record to be compared. */ { printf ("%s area: '%s' Last Written: %s", name_ptr, files[file_no].name_ptr, files[file_no].lw_ptr); if ( files[file_no].text_ptr ) { printf (" Text: '%s'", files[file_no].text_ptr ); } (void) putchar ('\n'); } @//E*O*F pass5.c// chmod u=rw,g=rw,o=rw pass5.c exit 0