
com.imsweb.algorithms.survival.SurvivalTimeUtils Maven / Gradle / Ivy
/*
* Copyright (C) 2012 Information Management Services, Inc.
*/
package com.imsweb.algorithms.survival;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
/**
* This class is used to calculate the survival time in months for a give patient (a list of records).
* It also calculates the vital status of the patient at the study cutoff date.
*
* See https://seer.cancer.gov/survivaltime/.
*/
public final class SurvivalTimeUtils {
public static final String ALG_NAME = "SEER Survival Time in Months";
public static final String VERSION = "version 2.2 released in September 2014";
public static final String SURVIVAL_FLAG_COMPLETE_INFO_NO_SURVIVAL = "0";
public static final String SURVIVAL_FLAG_COMPLETE_INFO_SOME_SURVIVAL = "1";
public static final String SURVIVAL_FLAG_MISSING_INFO_NO_SURVIVAL_POSSIBLE = "2";
public static final String SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL = "3";
public static final String SURVIVAL_FLAG_DCO_AUTOPSY_ONLY = "8";
public static final String SURVIVAL_FLAG_UNKNOWN = "9";
private static final double _DAYS_IN_MONTH = 365.24 / 12;
public static final String UNKNOWN_SURVIVAL = "9999";
public static final String BLANK_YEAR = null;
public static final String BLANK_MONTH = null;
public static final String BLANK_DAY = null;
private SurvivalTimeUtils() {
// no instances of this class allowed!
}
/**
* Calculates the survival time for the provided list of records representing a patient.
* It also calculates the vital status of the patient at the study cutoff date.
*
* See https://seer.cancer.gov/survivaltime/
* @param input input object representing a patient
* @param endPointYear end point year (also called end study year, or current reporting year)
* @return SurvivalTimeOutputPatientDto
*/
public static SurvivalTimeOutputPatientDto calculateSurvivalTime(SurvivalTimeInputPatientDto input, int endPointYear) {
SurvivalTimeOutputPatientDto patientResultsDto = new SurvivalTimeOutputPatientDto();
if (input == null || input.getSurvivalTimeInputPatientDtoList().isEmpty())
return patientResultsDto;
List allRecords = input.getSurvivalTimeInputPatientDtoList();
List patientResultsList = new ArrayList<>();
try {
// lets check if all records of a patient have same dolc and vs
String dolcYearStr = allRecords.get(0).getDateOfLastContactYear();
String dolcMonthStr = allRecords.get(0).getDateOfLastContactMonth();
String dolcDayStr = allRecords.get(0).getDateOfLastContactDay();
String vsStr = allRecords.get(0).getVitalStatus();
patientResultsDto.setVitalStatusRecode(vsStr);
for (SurvivalTimeInputRecordDto rec : allRecords) {
boolean sameYear = dolcYearStr == null ? rec.getDateOfLastContactYear() == null : dolcYearStr.equals(rec.getDateOfLastContactYear());
boolean sameMonth = dolcMonthStr == null ? rec.getDateOfLastContactMonth() == null : dolcMonthStr.equals(rec.getDateOfLastContactMonth());
boolean sameDay = dolcDayStr == null ? rec.getDateOfLastContactDay() == null : dolcDayStr.equals(rec.getDateOfLastContactDay());
boolean sameVs = vsStr == null ? rec.getVitalStatus() == null : vsStr.equals(rec.getVitalStatus());
if (!sameYear || !sameMonth || !sameDay || !sameVs) {
//We need to sort the records to calculated the sorted index
List tempInternalRecords = new ArrayList<>();
for (SurvivalTimeInputRecordDto orgRecord : allRecords) {
SurvivalTimeOutputRecordDto recordResult = new SurvivalTimeOutputRecordDto();
recordResult.setSurvivalTimeDxYear(BLANK_YEAR);
recordResult.setSurvivalTimeDxMonth(BLANK_MONTH);
recordResult.setSurvivalTimeDxDay(BLANK_DAY);
recordResult.setSurvivalTimeDolcYear(BLANK_YEAR);
recordResult.setSurvivalTimeDolcMonth(BLANK_MONTH);
recordResult.setSurvivalTimeDolcDay(BLANK_DAY);
recordResult.setSurvivalTimeDolcYearPresumedAlive(BLANK_YEAR);
recordResult.setSurvivalTimeDolcMonthPresumedAlive(BLANK_MONTH);
recordResult.setSurvivalTimeDolcDayPresumedAlive(BLANK_DAY);
recordResult.setSurvivalMonths(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlag(SURVIVAL_FLAG_UNKNOWN);
recordResult.setSurvivalMonthsPresumedAlive(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_UNKNOWN);
patientResultsList.add(recordResult);
tempInternalRecords.add(new InternalRecDto(orgRecord, recordResult));
}
//Let's sort the temp records list and assign back the sorted index to the output
tempInternalRecords = sortTempRecords(tempInternalRecords);
for (int sortedIdx = 1; sortedIdx <= tempInternalRecords.size(); sortedIdx++) // output sort index should be 1-based
tempInternalRecords.get(sortedIdx - 1)._recordResult.setSortedIndex(sortedIdx);
patientResultsDto.setSurvivalTimeOutputPatientDtoList(patientResultsList);
if (!sameVs)
patientResultsDto.setVitalStatusRecode(null);
return patientResultsDto;
}
}
// Lets continue if all records have same dolc and vs
int dolcYear = NumberUtils.isDigits(dolcYearStr) ? Integer.parseInt(dolcYearStr) : 9999;
int dolcMonth = NumberUtils.isDigits(dolcMonthStr) ? Integer.parseInt(dolcMonthStr) : 99;
int dolcDay = NumberUtils.isDigits(dolcDayStr) ? Integer.parseInt(dolcDayStr) : 99;
int vs = NumberUtils.isDigits(vsStr) ? Integer.parseInt(vsStr) : 9;
//if dolc year is invalid or if dolc date is future date, set it as missing, and if Dolc year is missing set the month and date as missing.
boolean isDolcValid = isDateValid(dolcYear, dolcMonth, dolcDay);
LocalDate now = LocalDate.now();
if (dolcYear < 1900 || dolcYear > now.getYear() || (dolcYear == now.getYear() && ((dolcMonth <= 12 && dolcMonth > now.getMonthValue()) || (dolcMonth == now.getMonthValue() && isDolcValid
&& dolcDay > now.getDayOfMonth()))))
dolcYear = 9999;
if (dolcYear == 9999)
dolcMonth = dolcDay = 99;
//Vital status recode is vital status at the study cutoff date.
if (dolcYear != 9999 && dolcYear > endPointYear)
patientResultsDto.setVitalStatusRecode("1");
// we are also going to need the birth date for filling the missing diagnosis and dolc dates. (#192)
String birthYearStr = allRecords.isEmpty() ? "" : allRecords.get(0).getBirthYear();
int birthYear = NumberUtils.isDigits(birthYearStr) ? Integer.parseInt(birthYearStr) : 9999;
String birthMonthStr = allRecords.isEmpty() ? "" : allRecords.get(0).getBirthMonth();
int birthMonth = NumberUtils.isDigits(birthMonthStr) ? Integer.parseInt(birthMonthStr) : 99;
String birthDayStr = allRecords.isEmpty() ? "" : allRecords.get(0).getBirthDay();
int birthDay = NumberUtils.isDigits(birthDayStr) ? Integer.parseInt(birthDayStr) : 99;
// we are going to use DTO objects to make it easier, let's also use a different list so we don't change the order of the original records
List validTempRecords = new ArrayList<>();
List allTempRecords = new ArrayList<>();
for (SurvivalTimeInputRecordDto orgRecord : allRecords) {
SurvivalTimeOutputRecordDto recordResult = new SurvivalTimeOutputRecordDto();
recordResult.setSurvivalTimeDxYear(orgRecord.getDateOfDiagnosisYear());
recordResult.setSurvivalTimeDxMonth(orgRecord.getDateOfDiagnosisMonth());
recordResult.setSurvivalTimeDxDay(orgRecord.getDateOfDiagnosisDay());
recordResult.setSurvivalTimeDolcYear(orgRecord.getDateOfLastContactYear());
recordResult.setSurvivalTimeDolcMonth(orgRecord.getDateOfLastContactMonth());
recordResult.setSurvivalTimeDolcDay(orgRecord.getDateOfLastContactDay());
recordResult.setSurvivalTimeDolcYearPresumedAlive(orgRecord.getDateOfLastContactYear());
recordResult.setSurvivalTimeDolcMonthPresumedAlive(orgRecord.getDateOfLastContactMonth());
recordResult.setSurvivalTimeDolcDayPresumedAlive(orgRecord.getDateOfLastContactDay());
patientResultsList.add(recordResult);
InternalRecDto tempRec = new InternalRecDto(orgRecord, recordResult);
allTempRecords.add(tempRec);
// check validity of the date
boolean valid = tempRec._year != 9999 && tempRec._year >= 1900 && tempRec._year <= endPointYear;
if (valid && !isDateValid(tempRec._year, tempRec._month == 99 ? 1 : tempRec._month, tempRec._day == 99 ? 1 : tempRec._day)) {
if (tempRec._month >= 1 && tempRec._month <= 12) {
tempRec._daySafe = 99;
tempRec._day = 99;
}
else {
tempRec._month = 99;
tempRec._monthSafe = 99;
tempRec._daySafe = 99;
tempRec._day = 99;
}
}
if (!valid) { // any "bad" record is assigned unknown results
recordResult.setSurvivalMonths(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlag(SURVIVAL_FLAG_UNKNOWN);
recordResult.setSurvivalMonthsPresumedAlive(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_UNKNOWN);
recordResult.setSurvivalTimeDxYear(BLANK_YEAR);
recordResult.setSurvivalTimeDxMonth(BLANK_MONTH);
recordResult.setSurvivalTimeDxDay(BLANK_DAY);
}
else
validTempRecords.add(tempRec);
}
// STEP 1 - sort the records by DX date (and/or sequence number)
validTempRecords = sortTempRecords(validTempRecords);
// calculate the variables without presuming ALIVE (this one cannot handle an unknown DOLC)
if (dolcYear != 9999)
calculateSurvivalTime(validTempRecords, patientResultsList, dolcYear, dolcMonth, dolcDay, birthYear, birthMonth, birthDay, vs, endPointYear, 12, 31, false);
else {
for (InternalRecDto rec : validTempRecords) {
rec._recordResult.setSurvivalMonths(UNKNOWN_SURVIVAL);
rec._recordResult.setSurvivalMonthsFlag(SURVIVAL_FLAG_UNKNOWN);
rec._recordResult.setSurvivalTimeDolcYear(BLANK_YEAR);
rec._recordResult.setSurvivalTimeDolcMonth(BLANK_MONTH);
rec._recordResult.setSurvivalTimeDolcDay(BLANK_DAY);
}
}
// reset the computed dates
for (InternalRecDto rec : validTempRecords) {
rec._yearSafe = rec._year;
rec._monthSafe = rec._month;
rec._daySafe = rec._day;
}
// calculate the variables presuming ALIVE (this one can handle the unknown DOLC only if vital status is ALIVE)
if (dolcYear != 9999 || vs == 1)
calculateSurvivalTime(validTempRecords, patientResultsList, dolcYear, dolcMonth, dolcDay, birthYear, birthMonth, birthDay, vs, endPointYear, 12, 31, true);
else {
for (InternalRecDto rec : validTempRecords) {
rec._recordResult.setSurvivalMonthsPresumedAlive(UNKNOWN_SURVIVAL);
rec._recordResult.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_UNKNOWN);
rec._recordResult.setSurvivalTimeDolcYearPresumedAlive(BLANK_YEAR);
rec._recordResult.setSurvivalTimeDolcMonthPresumedAlive(BLANK_MONTH);
rec._recordResult.setSurvivalTimeDolcDayPresumedAlive(BLANK_DAY);
}
}
//Set the flags for DCO/Autopsy only cases (type of reporting source 6 and 7) to 8 and set the survival months fields to missing. (#192)
for (InternalRecDto rec : validTempRecords) {
if ("6".equals(rec._originalRecord.getTypeOfReportingSource()) || "7".equals(rec._originalRecord.getTypeOfReportingSource())) {
rec._recordResult.setSurvivalMonths(UNKNOWN_SURVIVAL);
rec._recordResult.setSurvivalMonthsFlag(SURVIVAL_FLAG_DCO_AUTOPSY_ONLY);
rec._recordResult.setSurvivalMonthsPresumedAlive(UNKNOWN_SURVIVAL);
rec._recordResult.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_DCO_AUTOPSY_ONLY);
}
}
// assign the sorted index on every output record: based on sorted tmp records
allTempRecords = sortTempRecords(allTempRecords);
for (int sortedIdx = 1; sortedIdx <= allTempRecords.size(); sortedIdx++) // output sort index should be 1-based
allTempRecords.get(sortedIdx - 1)._recordResult.setSortedIndex(sortedIdx);
}
catch (DateTimeException e) {
// final safety net, if anything goes wrong, just assign 9's
patientResultsList.clear();
int sortedIdx = 1; // output sort index should be 1-based
for (SurvivalTimeInputRecordDto orgRecord : allRecords) {
SurvivalTimeOutputRecordDto recordResult = new SurvivalTimeOutputRecordDto();
recordResult.setSurvivalMonths(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlag(SURVIVAL_FLAG_UNKNOWN);
recordResult.setSurvivalMonthsPresumedAlive(UNKNOWN_SURVIVAL);
recordResult.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_UNKNOWN);
recordResult.setSurvivalTimeDxYear(orgRecord.getDateOfDiagnosisYear());
recordResult.setSurvivalTimeDxMonth(orgRecord.getDateOfDiagnosisMonth());
recordResult.setSurvivalTimeDxDay(orgRecord.getDateOfDiagnosisDay());
recordResult.setSurvivalTimeDolcYear(orgRecord.getDateOfLastContactYear());
recordResult.setSurvivalTimeDolcMonth(orgRecord.getDateOfLastContactMonth());
recordResult.setSurvivalTimeDolcDay(orgRecord.getDateOfLastContactDay());
recordResult.setSurvivalTimeDolcYearPresumedAlive(orgRecord.getDateOfLastContactYear());
recordResult.setSurvivalTimeDolcMonthPresumedAlive(orgRecord.getDateOfLastContactMonth());
recordResult.setSurvivalTimeDolcDayPresumedAlive(orgRecord.getDateOfLastContactDay());
recordResult.setSortedIndex(sortedIdx++);
patientResultsList.add(recordResult);
}
}
patientResultsDto.setSurvivalTimeOutputPatientDtoList(patientResultsList);
return patientResultsDto;
}
@SuppressWarnings({"SameParameterValue", "java:S107", "java:S3776"}) // too many arguments, method too complex
private static void calculateSurvivalTime(List records, List patientResultsList, int dolcYear, int dolcMonth, int dolcDay, int birthYear, int birthMonth, int birthDay, int vs, int endYear, int endMonth, int endDay, boolean presumeAlive) {
//check validity of DOLC
//if the month is invalid, set both day and month to 99, if the day is invalid set it as missing.
if (!isDateValid(dolcYear == 9999 ? 1900 : dolcYear, dolcMonth == 99 ? 1 : dolcMonth, dolcDay == 99 ? 1 : dolcDay)) {
if (dolcMonth >= 1 && dolcMonth <= 12)
dolcDay = 99;
else
dolcMonth = dolcDay = 99;
}
// the DOLC also needs to be fixed using the same logic as the DX dates, so let's create a fake record at the end of the list
InternalRecDto dolc = new InternalRecDto();
dolc._year = dolc._yearSafe = dolcYear;
dolc._month = dolc._monthSafe = dolcMonth;
dolc._day = dolc._daySafe = dolcDay;
records.add(dolc);
// STEP 2 - fill in unknown parts
//lets use the birthdate to fix the missing dx and dolc (#192)
//check validity of month and day and set them as missing if they are invalid
if (birthYear == 9999)
birthMonth = birthDay = 99;
if (birthMonth == 99)
birthDay = 99;
if (!isDateValid(birthYear, birthMonth == 99 ? 1 : birthMonth, birthDay == 99 ? 1 : birthDay)) {
if (birthMonth >= 1 && birthMonth <= 12)
birthDay = 99;
else
birthMonth = birthDay = 99;
}
// step 2.1 - assign unknown day for dates that have a known month, skip the birthday
for (int i = 0; i < records.size(); i++) {
InternalRecDto rec = records.get(i);
if (rec._monthSafe != 99 && rec._daySafe == 99) {
// get earliest day possible for this date
int earliestDayPossible = 0;
//use birthday for the first record
if (i == 0 && birthYear == rec._yearSafe && birthMonth == rec._monthSafe && birthDay != 99)
earliestDayPossible = birthDay;
for (int j = i - 1; j >= 0; j--) {
InternalRecDto prevRec = records.get(j);
if (prevRec._daySafe != 99) {
if (prevRec._yearSafe == rec._yearSafe && prevRec._monthSafe == rec._monthSafe)
earliestDayPossible = prevRec._daySafe;
break;
}
}
if (earliestDayPossible == 0)
earliestDayPossible = 1;
// get latest day possible for this date
int latestDayPossible = 0;
for (int j = i + 1; j < records.size(); j++) {
InternalRecDto nextRec = records.get(j);
if (nextRec._daySafe != 99) {
if (nextRec._yearSafe == rec._yearSafe && nextRec._monthSafe == rec._monthSafe)
latestDayPossible = nextRec._daySafe;
break;
}
}
if (latestDayPossible == 0)
latestDayPossible = YearMonth.of(rec._yearSafe, rec._monthSafe).lengthOfMonth();
// take the middle point between earliest and latest
rec._daySafe = (int)Math.floor((double)(earliestDayPossible + latestDayPossible) / (double)2);
/* SP_CHANGE - SAS--Overwrite the dates with the new dates.
JAVA--Sewbesew and Fabian talked and decided not to modify the original record.
We decided to return an output dto with 4 calculated values and modified dates
Changes will be to dates that were originally unknown. */
rec._recordResult.setSurvivalTimeDxDay(StringUtils.leftPad(String.valueOf(rec._daySafe), 2, "0"));
}
}
// step 2.2 - assign unknown month and day for dates that also have unknown month, skip the birthday
for (int i = 0; i < records.size(); i++) {
InternalRecDto rec = records.get(i);
if (rec._monthSafe == 99) {
// get earliest day and month possible for this date
int earliestMonthPossible = 0;
int earliestDayPossible = 0;
//use birthday for the first record
if (i == 0 && birthYear == rec._yearSafe && birthMonth != 99) {
earliestMonthPossible = birthMonth;
earliestDayPossible = birthDay != 99 ? birthDay : 1;
}
for (int j = i - 1; j >= 0; j--) {
InternalRecDto prevRec = records.get(j);
if (prevRec._monthSafe != 99) {
if (prevRec._yearSafe == rec._yearSafe) {
earliestMonthPossible = prevRec._monthSafe;
earliestDayPossible = prevRec._daySafe;
}
break;
}
}
if (earliestMonthPossible == 0) {
earliestMonthPossible = 1;
earliestDayPossible = 1;
}
// get latest day and month possible for this date
int latestMonthPossible = 0;
int latestDayPossible = 0;
for (int j = i + 1; j < records.size(); j++) {
InternalRecDto nextRec = records.get(j);
if (nextRec._monthSafe != 99) {
if (nextRec._yearSafe == rec._yearSafe) {
latestMonthPossible = nextRec._monthSafe;
latestDayPossible = nextRec._daySafe;
}
break;
}
}
if (latestMonthPossible == 0) {
latestMonthPossible = 12;
latestDayPossible = 31;
}
// take the middle point between earliest and latest
LocalDate earliestDate = LocalDate.of(rec._yearSafe, earliestMonthPossible, earliestDayPossible);
int diffInDays = (int)ChronoUnit.DAYS.between(earliestDate, LocalDate.of(rec._yearSafe, latestMonthPossible, latestDayPossible));
earliestDate = earliestDate.plusDays((int)Math.floor(diffInDays / 2.0));
rec._monthSafe = earliestDate.getMonthValue();
rec._daySafe = earliestDate.getDayOfMonth();
/* SP_CHANGE - SAS--Overwrite the dates with the new dates.
JAVA--Sewbesew and Fabian talked and decided not to modify the original record.
We decided to return an output dto with 4 calculated values and modified dates.
Changes will be to dates that were originally unknown. */
rec._recordResult.setSurvivalTimeDxMonth(StringUtils.leftPad(String.valueOf(rec._monthSafe), 2, "0"));
rec._recordResult.setSurvivalTimeDxDay(StringUtils.leftPad(String.valueOf(rec._daySafe), 2, "0"));
}
}
// we are done with the fake DOLC too , let's remove it
records.remove(records.size() - 1);
//and reset the filled day and month of DOLC if year is unknown
if (dolcYear == 9999)
dolc._monthSafe = dolc._daySafe = 99;
/* if date of last contact is beyond study cut-off, set to study cut-off for both calculations */
if (dolcYear > endYear && dolcYear != 9999) {
dolc._yearSafe = endYear;
dolc._monthSafe = endMonth;
dolc._daySafe = endDay;
}
//set the modified DOLC to all record results
for (SurvivalTimeOutputRecordDto output : patientResultsList) {
if (dolc._yearSafe != 9999) {
output.setSurvivalTimeDolcYear(StringUtils.leftPad(String.valueOf(dolc._yearSafe), 4, "0"));
output.setSurvivalTimeDolcMonth(StringUtils.leftPad(String.valueOf(dolc._monthSafe), 2, "0"));
output.setSurvivalTimeDolcDay(StringUtils.leftPad(String.valueOf(dolc._daySafe), 2, "0"));
}
}
/* if patient alive and presumed alive, set to study cut-off for presumed alive */
if (presumeAlive && vs == 1) {
dolc._yearSafe = endYear;
dolc._monthSafe = endMonth;
dolc._daySafe = endDay;
}
//set the modified DOLC presumed alive to all record results
for (SurvivalTimeOutputRecordDto output : patientResultsList) {
if (dolc._yearSafe != 9999) {
output.setSurvivalTimeDolcYearPresumedAlive(StringUtils.leftPad(String.valueOf(dolc._yearSafe), 4, "0"));
output.setSurvivalTimeDolcMonthPresumedAlive(StringUtils.leftPad(String.valueOf(dolc._monthSafe), 2, "0"));
output.setSurvivalTimeDolcDayPresumedAlive(StringUtils.leftPad(String.valueOf(dolc._daySafe), 2, "0"));
}
}
// STEP 3 - calculate variables for each record
for (InternalRecDto rec : records) {
int diffInDays = (int)ChronoUnit.DAYS.between(LocalDate.of(rec._yearSafe, rec._monthSafe, rec._daySafe), LocalDate.of(dolc._yearSafe, dolc._monthSafe, dolc._daySafe));
if (presumeAlive)
rec._diffInDaysPA = diffInDays;
else
rec._diffInDays = diffInDays;
int diffInMonth = (int)Math.floor(diffInDays / _DAYS_IN_MONTH);
//if we use end point dates as DOLC for calculation, use that date to calculate the flags too.
if (dolc._yearSafe == endYear && dolc._monthSafe == endMonth && dolc._daySafe == endDay) {
dolc._year = endYear;
dolc._month = endMonth;
dolc._day = endDay;
}
// safety net - do not allow negative values
if (diffInMonth < 0)
diffInMonth = 9999; // unknown
// evaluate the flag
String flag;
if (diffInMonth == 9999)
flag = SURVIVAL_FLAG_UNKNOWN; // flag=9
else if (rec._month == 99 || rec._day == 99 || dolc._month == 99 || dolc._day == 99) {
if (rec._year == dolc._year && (rec._month == dolc._month || rec._month == 99 || dolc._month == 99))
flag = SURVIVAL_FLAG_MISSING_INFO_NO_SURVIVAL_POSSIBLE; // flag=2
else
flag = SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL; // flag=3
}
else if (diffInDays > 0)
flag = SURVIVAL_FLAG_COMPLETE_INFO_SOME_SURVIVAL; // flag=1
else
flag = SURVIVAL_FLAG_COMPLETE_INFO_NO_SURVIVAL; // flag=0
if (presumeAlive) {
rec._recordResult.setSurvivalMonthsPresumedAlive(StringUtils.leftPad(String.valueOf(diffInMonth), 4, "0"));
rec._recordResult.setSurvivalMonthsFlagPresumedAlive(flag);
}
else {
rec._recordResult.setSurvivalMonths(StringUtils.leftPad(String.valueOf(diffInMonth), 4, "0"));
rec._recordResult.setSurvivalMonthsFlag(flag);
}
}
// STEP 4 - go through the records IN REVERSE ORDER and fix the issue where a person could have a DX with some missing codes
// coded as "could be 0 days" followed by a tumor that could not be zero days (with or without some missing) - therefore the
// earlier tumor can't be 0 days.
boolean survivalGreaterThanZero = false;
boolean survivalGreaterThanZeroPA = false;
for (int i = records.size() - 1; i >= 0; i--) {
SurvivalTimeOutputRecordDto rec = records.get(i)._recordResult;
String flag = rec.getSurvivalMonthsFlag();
String flagPA = rec.getSurvivalMonthsFlagPresumedAlive();
if (SURVIVAL_FLAG_COMPLETE_INFO_SOME_SURVIVAL.equals(flag) || SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL.equals(flag))
survivalGreaterThanZero = true;
if (SURVIVAL_FLAG_COMPLETE_INFO_SOME_SURVIVAL.equals(flagPA) || SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL.equals(flagPA))
survivalGreaterThanZeroPA = true;
if (SURVIVAL_FLAG_MISSING_INFO_NO_SURVIVAL_POSSIBLE.equals(flag) && survivalGreaterThanZero)
rec.setSurvivalMonthsFlag(SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL);
if (SURVIVAL_FLAG_MISSING_INFO_NO_SURVIVAL_POSSIBLE.equals(flagPA) && survivalGreaterThanZeroPA)
rec.setSurvivalMonthsFlagPresumedAlive(SURVIVAL_FLAG_MISSING_INFO_SOME_SURVIVAL);
}
}
private static class InternalRecDto implements Comparable {
SurvivalTimeInputRecordDto _originalRecord;
SurvivalTimeOutputRecordDto _recordResult;
int _year;
int _month;
int _day;
int _yearSafe;
int _monthSafe;
int _daySafe;
int _seqNum;
int _diffInDays;
int _diffInDaysPA;
public InternalRecDto() {
_recordResult = new SurvivalTimeOutputRecordDto();
}
public InternalRecDto(SurvivalTimeInputRecordDto rec, SurvivalTimeOutputRecordDto recordResult) {
_originalRecord = rec;
_recordResult = recordResult;
String yearStr = rec.getDateOfDiagnosisYear();
String monthStr = rec.getDateOfDiagnosisMonth();
String dayStr = rec.getDateOfDiagnosisDay();
_year = _yearSafe = NumberUtils.isDigits(yearStr) ? Integer.parseInt(yearStr) : 9999;
_month = _monthSafe = NumberUtils.isDigits(monthStr) ? Integer.parseInt(monthStr) : 99;
_day = _daySafe = NumberUtils.isDigits(dayStr) ? Integer.parseInt(dayStr) : 99;
if (_month == 99)
_day = _daySafe = 99;
// sequence number
// the sequence numbers might be used to determine the order; there are two families of sequences: federal (00-59, 98, 99) and non-federal (60-97);
// sine the non-federal need to always be after the federal, let's add 100 to all the non-federal (making them 160-197)
String seqNumStr = rec.getSequenceNumberCentral();
_seqNum = NumberUtils.isDigits(seqNumStr) ? Integer.parseInt(seqNumStr) : -1;
if (_seqNum >= 60 && _seqNum <= 97)
_seqNum = _seqNum + 100;
}
@Override
public int compareTo(InternalRecDto other) {
if (_year == 9999 || other._year == 9999)
return _seqNum - other._seqNum;
else if (_year != other._year)
return _year - other._year;
else {
if (_month == 99 || other._month == 99)
return _seqNum - other._seqNum;
else if (_month != other._month)
return _month - other._month;
else {
if (_day == 99 || other._day == 99 || this._day == other._day)
return _seqNum - other._seqNum;
else
return _day - other._day;
}
}
}
public int compareDateOnly(InternalRecDto other) {
if (_year == 9999 || other._year == 9999)
return 0;
else if (_year != other._year)
return _year - other._year;
else {
if (_month == 99 || other._month == 99)
return 0;
else if (_month != other._month)
return _month - other._month;
else {
if (_day == 99 || other._day == 99 || this._day == other._day)
return 0;
else
return _day - other._day;
}
}
}
@Override
@SuppressWarnings("SimplifiableIfStatement")
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
InternalRecDto other = (InternalRecDto)o;
if (_year == 9999 || other._year == 9999)
return _seqNum == other._seqNum;
else if (_year != other._year)
return false;
else {
if (_month == 99 || other._month == 99)
return _seqNum == other._seqNum;
else if (_month != other._month)
return false;
else {
if (_day == 99 || other._day == 99 || this._day == other._day)
return _seqNum == other._seqNum;
else
return false;
}
}
}
@Override
public int hashCode() {
int result = _year;
result = 31 * result + _month;
result = 31 * result + _day;
result = 31 * result + _seqNum;
return result;
}
}
/**
* This method is added to avoid the situation where a > b and b > c and c > a
* Valid dates are always ordered first and then we add the invalid ones, we compare the invalid one starting from the last one
* Example:
* rec 1: 01: 2015/3/5
* rec 2: 02: 2015/99/99
* rec 3: 60: 2015/2/23
* We want 3 and 1 to be compared first which 3 is first and 1 is second. (3, 1)
* Then we compare the invalid one starting from the last, that is rec 1 in this case.
* The order will become 3, 1, 2
*/
private static List sortTempRecords(List list) {
List validRecords = new ArrayList<>();
List dayMissingRecords = new ArrayList<>();
List monthMissingRecords = new ArrayList<>();
List yearMissingRecords = new ArrayList<>();
for (InternalRecDto dto : list) {
if (dto._year == 9999)
yearMissingRecords.add(dto);
else if (dto._month == 99)
monthMissingRecords.add(dto);
else if (dto._day == 99)
dayMissingRecords.add(dto);
else
validRecords.add(dto);
}
List result = new ArrayList<>(validRecords);
Collections.sort(result);
//Handle day missing records
sortTempRecords(result, dayMissingRecords);
//Handle Month missing records
sortTempRecords(result, monthMissingRecords);
//Handle year missing record
sortTempRecords(result, yearMissingRecords);
return result;
}
private static void sortTempRecords(List result, List subList) {
Collections.sort(subList);
if (result.isEmpty())
result.addAll(subList);
else {
for (InternalRecDto r : subList) {
for (int i = result.size() - 1; i >= 0; i--) {
//if the subList record's date is later than the sorted result record, append it after sorted record
if (r.compareDateOnly(result.get(i)) > 0) {
result.add(i + 1, r);
break;
}
else if (r.compareDateOnly(result.get(i)) == 0) {
if ((r._seqNum < 100 && r._seqNum >= result.get(i)._seqNum) || (r._seqNum > 100 && result.get(i)._seqNum > 100 && r._seqNum >= result.get(i)._seqNum)) {
result.add(i + 1, r);
break;
}
else if (r._seqNum > 100) {
List sortedRecordsWithPotentialLaterDate = new ArrayList<>();
for (int j = 0; j <= i; j++)
if (r.compareDateOnly(result.get(j)) == 0)
sortedRecordsWithPotentialLaterDate.add(result.get(j));
if (!hasConflictedSequence(r._seqNum, sortedRecordsWithPotentialLaterDate)) {
result.add(i + 1, r);
break;
}
}
}
if (i == 0)
result.add(0, r);
}
}
}
}
//This method is only for non federal, it checks if current non-federal should go before another non-federal
private static boolean hasConflictedSequence(int seq, List sortedResult) {
//if all other records are federal, the non federal should go last
if (sortedResult.stream().noneMatch(r -> r._seqNum > 100))
return false;
//if the other non federal's are not in the right order, don't bother fixing this
for (int i = 0; i < sortedResult.size() - 1; i++)
for (int j = i + 1; j <= sortedResult.size() - 1; j++)
if (sortedResult.get(i)._seqNum > 100 && sortedResult.get(j)._seqNum > 100 && sortedResult.get(i)._seqNum > sortedResult.get(j)._seqNum)
return false;
//If they are in order and if current sequence number is less than one of them, we need to move the current record
for (int i = sortedResult.size() - 1; i >= 0; i--)
if (sortedResult.get(i)._seqNum > 100)
return seq <= sortedResult.get(i)._seqNum;
return false;
}
@SuppressWarnings({"java:S2201", "ResultOfMethodCallIgnored"}) // ignored return value
private static boolean isDateValid(int year, int month, int day) {
try {
LocalDate.of(year, month, day);
}
catch (DateTimeException e) {
return false;
}
return true;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy