int table_offset = invalid;
std::streampos table_offset(-1);
std::vector<double> actuarial_table    (std::string const& a_table_filename    ,int                a_table_number    ,int                a_age    ,int                a_len    ){    // SOA documentation does not specify the domain of table numbers,    // but their tables seem to use only positive integers    // representable as 32-bit signed int, so take that as the range.    // Asserting that the table number is nonzero makes it safe to use    // zero as a sentry.    LMI_ASSERT(0 < a_table_number);    fs::path index_path(a_table_filename);    index_path = fs::change_extension(index_path, ".ndx");    fs::ifstream index_ifs(index_path, std::ios_base::in | std::ios_base::binary);    // Index records have fixed length:    //   4-byte integer:     table number    //   50-byte char array: table name    //   4-byte integer:     byte offset into '.dat' file    // Table numbers are not necessarily consecutive or sorted.    // TODO ?? Assert endianness too? SOA tables are not portable;    // probably they can easily be read only on x86 hardware.    LMI_ASSERT(8 == CHAR_BIT);    LMI_ASSERT(4 == sizeof(int));    LMI_ASSERT(2 == sizeof(short int));    int table_offset = invalid; // TODO ?? Reconsider type and initial value.    int const index_record_length(58);    char index_record[index_record_length] = {0};    while(index_ifs)        {        int index_table_number = *reinterpret_cast<boost::int32_t*>(index_record);        if(a_table_number == index_table_number)            {            table_offset = *reinterpret_cast<boost::int32_t*>(54 + index_record);            break;            }        index_ifs.read(index_record, index_record_length);        if(index_record_length != index_ifs.gcount())            {            std::ostringstream oss;            oss                << "Attempted to read "                << index_record_length                << " bytes, but got "                << index_ifs.gcount()                << " bytes instead."                ;            hobsons_choice() << oss.str() << LMI_FLUSH;            }        }    // Data records have variable length:    //   2-byte integer: record type    //   2-byte integer: nominal length    //   [type varies]:  data    //    // The record types of interest here are coded as:    //   9999 end of table    //   2    4-byte integer:  table_number    //   3    [unsigned] char: table_type: {A, D, S} --> {age, duration, select}    //   12   2-byte integer:  min_age    //   13   2-byte integer:  max_age    //   14   2-byte integer:  select_period    //   15   2-byte integer:  max_select_age (if zero, then it's max_age)    //   17   8-byte doubles:  values    //    // Record type 17, which stores values, is of special interest.    // The number of values equals the nominal length, in the SOA    // implementation. That means that no table can have more than    // 4096 values, which is a draconian restriction: 100 x 100    // tables are common enough in real-world practice.    //    // However, the actual number of values can always be deduced    // correctly from context. And the context is always known when    // the values are read, because the SOA implementation always    // writes the values after all records that identify the context.    // Therefore, the nominal length can be disregarded for record    // type 17, and any desired number of values written. If the    // actual number of values exceeds 4096, then this implementation    // handles them correctly, but the SOA implementation does not.    //    // GWC's email of Wednesday, December 16, 1998 5:56 PM to the    // author of the SOA implementation proposed a patch to overcome    // this limitation, but it was not accepted, and the limitation    // persists in later 32-bit versions of the software distributed    // by the SOA even as this is written on 2005-01-13.    fs::path data_path(a_table_filename);    data_path = fs::change_extension(data_path, ".dat");    fs::ifstream data_ifs(data_path, std::ios_base::in | std::ios_base::binary);    data_ifs.seekg(table_offset, std::ios::beg);    LMI_ASSERT(table_offset == data_ifs.tellg());    boost::int32_t table_number   = invalid;    unsigned char  table_type     = invalid;    boost::int16_t min_age        = invalid;    boost::int16_t max_age        = invalid;    boost::int16_t select_period  = invalid;    boost::int16_t max_select_age = invalid;    std::vector<double> values;// TODO ?? It would be an error to find more than one of each record, no?    while(data_ifs)        {        boost::int16_t record_type = invalid;        read(data_ifs, record_type, sizeof(boost::int16_t));        boost::int16_t nominal_length = invalid;        read(data_ifs, nominal_length, sizeof(boost::int16_t));        switch(record_type)          {            case 2: // 4-byte integer: table_number.                {                read(data_ifs, table_number, nominal_length);                LMI_ASSERT(table_number == a_table_number);                }                break;            case 3: // unsigned char: table_type.                {                // Meaning: {A, D, S} --> {age, duration, select}.                // SOA apparently permits upper or lower case.                read(data_ifs, table_type, nominal_length);                table_type = std::toupper(table_type);                LMI_ASSERT('A' == table_type || 'D' == table_type || 'S' == table_type);                }                break;            case 12: // 2-byte integer: min_age.                {                read(data_ifs, min_age, nominal_length);                LMI_ASSERT(0 <= min_age && min_age <= methuselah);                }                break;            case 13: // 2-byte integer: max_age.                {                read(data_ifs, max_age, nominal_length);                LMI_ASSERT(0 <= max_age && max_age <= methuselah);                }                break;            case 14: // 2-byte integer: select_period.                {                read(data_ifs, select_period, nominal_length);                LMI_ASSERT(0 <= select_period && select_period <= methuselah);                }                break;            case 15: // 2-byte integer: max_select_age.                {                read(data_ifs, max_select_age, nominal_length);                LMI_ASSERT(0 <= max_select_age && max_select_age <= methuselah);                }                break;            case 17: // 8-byte doubles: values.                {                read_values                    (values                    ,data_ifs                    ,nominal_length                    ,min_age                    ,max_age                    ,table_type                    ,max_select_age                    ,select_period                    );                }                break;            case 9999: // End of table.                {                goto done;                }                break;            default:                {                char skipped[65536];                data_ifs.read(skipped, nominal_length);                }          }        }    done:    LMI_ASSERT(min_age <= a_age && a_age <= max_age);    LMI_ASSERT(a_age + a_len <= max_age + 1);// TODO ?? Assert that there are enough values.    return particular_values                    (values                    ,data_ifs                    ,0                    ,min_age                    ,max_age                    ,table_type                    ,max_select_age                    ,select_period                    ,a_age                    ,a_len                    );}