package net.sf.egonet.persistence;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.egonet.model.Alter;
import net.sf.egonet.model.Answer;
import net.sf.egonet.model.Interview;
import net.sf.egonet.model.Question;
import net.sf.egonet.model.QuestionOption;
import net.sf.egonet.model.Question.QuestionType;
import net.sf.egonet.persistence.ExpressionNode.MathOp;
/**
* the survey question prompts can be altered at runtime using special tags,
* <VAR ... /> <COUNT ... /> <CALC ... /> <CONTAINS ... /> and <IF .. />
* This class contains static functions that will perform this
* 'insertion' of variable text
* @author Kevin
*/
public class TextInsertionUtil {
private static final String strMonths[] =
{"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December" };
private static final int monthCaps[] =
{31,28,31,30,31,30,31,31,30,31,30,31};
private enum DATE_FIELD { MONTH, DAY, YEAR, HOUR, MINUTE, NONE};
/**
* answerToQuestion
* a convenience function for answerInsertion below.
* Given the string that is a question title, it returns
* its answer in string form. The question is determined
* by the questions title and type ( strQuestionTitle, iType ).
* If questions are not found in the current section of the survey
* 'earlier' questions will be examined.
* If a question is not found in the ALTER or ALTER_PAIR section
* the EGO section will be searched
* If a question is not found in the EGO section
* the EGO_ID section will be searched
* @param strQuestionTitle - title of question we want the answer to
* @param interviewId - needed to look up variable values ( answers to previous questions )
* @param iType - needed to look up variable values ( answers to previous questions )
* @param studyId - needed to look up variable values ( answers to previous questions )
* @param listOfAlters - needed to look up variable values ( answers to previous questions )
* @return the questions answer in string form
*/
public static String answerToQuestion ( String strQuestionTitle,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
ArrayList<Alter> emptyAlters = new ArrayList<Alter>();
boolean isOtherSpecifyQuestion;
String otherSpecifyText;
Interview currentInterview;
StringTokenizer strok;
List<QuestionOption> optionsList;
Question question = null;
Answer theAnswer = null;
String strAnswer = strQuestionTitle;
String strOption;
Long iOptionId;
if ( interviewId==null )
return (strQuestionTitle);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strQuestionTitle);
// if listOfAlters is null
// that indicates this is being used in a preface.
// In the case of a preface look ONLY in EGO and EGO_ID
// for answers, regardless of where we come from
if ( listOfAlters==null && ( iType==QuestionType.ALTER || iType==QuestionType.ALTER_PAIR)) {
iType = QuestionType.EGO;
}
// if alter or alter_pair questions are used in a 'list of alters'
// format the listOfAlters will be null, or empty.
// in this case we can only deal with ego or ego_id questions.
if (( iType==QuestionType.ALTER || iType==QuestionType.ALTER_PAIR) && listOfAlters.isEmpty()) {
iType = QuestionType.EGO;
}
question = Questions.getQuestionUsingTitleAndTypeAndStudy (strQuestionTitle, iType, studyId);
if ( question==null && (iType==QuestionType.ALTER || iType==QuestionType.ALTER_PAIR)) {
iType = QuestionType.EGO;
question = Questions.getQuestionUsingTitleAndTypeAndStudy (strQuestionTitle, iType, studyId);
}
if ( question==null && iType==QuestionType.EGO ) {
iType = QuestionType.EGO_ID;
question = Questions.getQuestionUsingTitleAndTypeAndStudy (strQuestionTitle, iType, studyId);
}
if ( question==null )
return ("[ " +strQuestionTitle + " NOT FOUND]");
if ( iType==QuestionType.ALTER || iType==QuestionType.ALTER_PAIR) {
theAnswer = Answers.getAnswerForInterviewQuestionAlters( currentInterview, question, listOfAlters);
} else {
theAnswer = Answers.getAnswerForInterviewQuestionAlters( currentInterview, question, emptyAlters);
}
if ( theAnswer==null )
return("[ no answer to " + strQuestionTitle + " found]");
isOtherSpecifyQuestion = question.getOtherSpecify();
if ( theAnswer.getValue()==null ) {
switch ( theAnswer.getSkipReason()) {
case REFUSE: return ("(refuse)");
case DONT_KNOW: return ("(don't know)");
case NONE:
default: return("(unanswered)");
}
}
switch ( theAnswer.getAnswerType()) {
case SELECTION:
case MULTIPLE_SELECTION:
strAnswer = "";
optionsList = Options.getOptionsForQuestion(question.getId());
strok = new StringTokenizer(theAnswer.getValue(), ",");
while ( strok.hasMoreElements()) {
strOption = strok.nextToken();
try {
iOptionId = Long.parseLong(strOption);
} catch ( NumberFormatException nfe ) {
iOptionId = -1L;
}
for ( QuestionOption qo : optionsList ) {
if ( qo.getId().equals(iOptionId)) {
if ( strAnswer.length()>1 )
strAnswer += ", ";
// special check for 'other/specify' questions.
otherSpecifyText = null;
if ( isOtherSpecifyQuestion &&
qo.getName().trim().startsWith("OTHER SPECIFY"))
otherSpecifyText = theAnswer.getOtherSpecifyText();
if ( otherSpecifyText!=null && otherSpecifyText.length()>0 )
strAnswer += otherSpecifyText;
else
strAnswer += qo.getName();
}
}
}
break;
case TEXTUAL:
case NUMERICAL:
case DATE:
case TIME_SPAN:
strAnswer = theAnswer.getValue();
break;
}
return(strAnswer);
}
/**
* variableInsertion
* used with the tag <VAR ... />
* this will accept any arbitrary string and, if markers of the format <VAR ... />
* are found, will create a new string by substituting in answers.
* For example, if Question Q1 asked how many times last week a person smoked crack,
* a later question might be "Of the <VAR Q1 /> times you smoked crack last week, how many
* times did you also drink?"
* The pattern for embedded variables is <VAR ... />
* This is a static function in anticipation of cases where it has to be used on strings
* not immediately associated with this question.
* @param strInput - the original string to (possibly) alter
* @param interviewId - needed to look up variable values ( answers to previous questions )
* @param iType - needed to look up variable values ( answers to previous questions )
* @param studyId - needed to look up variable values ( answers to previous questions )
* @param listOfAlters - needed to look up variable values ( answers to previous questions )
* @return strInput but with any <VAR /> tags replaced with that variables value
*/
public static String variableInsertion (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
Interview currentInterview;
String strResult = "";
String pattern = "<VAR.*?/>";
String strVariableName;
String strVariableValue;
String str;
ArrayList<String> theList;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList ( strInput, pattern);
if (theList==null)
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replace the
// question names between the <VAR /> markers with the answer from
// that question
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strVariableName = trimPrefixAndSuffix ( str, "<VAR", "/>");
if ( strVariableName!=null ) {
strVariableValue = answerToQuestion(strVariableName, interviewId, iType, studyId, listOfAlters );
if ( strVariableValue == null || strVariableValue.length()==0 )
strVariableValue = "[no value for " + strVariableName + "]";
strResult += " " + strVariableValue + " ";
} else {
strResult += str;
}
}
return(strResult);
}
/**
* dateDataInsertion is very similar to variableInsertion but has more options and
* code specific to dates
* <DATE question /> will default to <VAR question /> behaviour
* <DATE question DATE_FIELD /> will print only the specific date field, such as the year
* <DATE question DATE_FIELD offset /> will print the specified date field offset from
* the one in the question by offset amount
* @param strInput - the original string to (possibly) alter
* @param interviewId - needed to look up variable values ( answers to previous questions )
* @param iType - needed to look up variable values ( answers to previous questions )
* @param studyId - needed to look up variable values ( answers to previous questions )
* @param listOfAlters - needed to look up variable values ( answers to previous questions )
* @return
*/
public static String dateDataInsertion (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
int Month = DATE_FIELD.MONTH.ordinal();
int Day = DATE_FIELD.DAY.ordinal();
int Year = DATE_FIELD.YEAR.ordinal();
Interview currentInterview;
String strResult = "";
String pattern = "<DATE.*?/>";
String strParameters; // question title, optional field specifier, optional offset
String[] paramInfo; // strParameters split into 3 parts
String strDate; // date as retrieved from answer Data
int[] dateInfo; // date split into year, month, day
int offset = 0;
String str;
DATE_FIELD dateField = DATE_FIELD.NONE;
ArrayList<String> theList;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList ( strInput, pattern);
if (theList==null)
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replacing the
// parameters between the <DATE /> markers with formatted DATE data
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strParameters = trimPrefixAndSuffix ( str, "<DATE", "/>");
if ( strParameters==null ) {
strResult += str;
} else {
if ( strParameters.length()==0) {
strResult += "[empty <DATE /> tag]";
} else {
paramInfo = strParameters.split(" ");
if ( paramInfo.length>1 ) {
try { dateField = DATE_FIELD.valueOf(paramInfo[1]);
} catch ( java.lang.RuntimeException rte ) {
strResult += "[" + paramInfo[1] + " should be YEAR, MONTH, or DAY]";
}
}
if ( paramInfo.length>2 ) {
if ( paramInfo[2].startsWith("+") && paramInfo[2].length()>1 )
paramInfo[2] = paramInfo[2].substring(1).trim();
try { offset = Integer.parseInt(paramInfo[2]);
} catch ( java.lang.RuntimeException rte ) {
offset = 0;
}
}
strDate = answerToQuestion(paramInfo[0], interviewId, iType, studyId, listOfAlters );
if ( strDate==null || strDate.length()==0 ) {
strResult += "[date "+strDate+" not found]";
} else {
dateInfo = strDateToNumeric(strDate);
switch ( paramInfo.length ) {
case 0:
case 1:
strResult += " " + strDate + " ";
break;
case 2: {
switch ( dateField ) {
case NONE: break;
case MONTH:
if ( dateInfo[Month]<0 || dateInfo[Month]>11)
strResult += " " + dateInfo[Month] + " ";
else
strResult += " " + strMonths[dateInfo[Month]] + " ";
break;
case DAY: strResult += " " + dateInfo[Day] + " "; break;
case YEAR: strResult += " " + dateInfo[Year] + " "; break;
}
}
break;
case 3: {
int[] newDate;
newDate = applyOffsetToDate ( dateInfo, dateField, offset );
switch ( dateField ) {
case NONE: break;
case MONTH:
if ( dateInfo[Month]<0 || dateInfo[Month]>11)
strResult += " " + dateInfo[Month] + " ";
else
strResult += " " + strMonths[newDate[Month]] + " ";
if ( newDate[Year] != dateInfo[Year] )
strResult += newDate[Year] + " ";
break;
case DAY:
if ( newDate[Month] < 0 || newDate[Month]>11 )
strResult += " " + newDate[Month] + " " + newDate[Day] + " ";
else
strResult += " " + strMonths[newDate[Month]] + " " + newDate[Day] + " ";
if ( newDate[Year] != dateInfo[Year] )
strResult += newDate[Year] + " ";
break;
case YEAR:
strResult += " " + newDate[Year] + " ";
break;
}
}
}
}
}
}
}
return(strResult);
}
/**
* calculationInsertion
* used with the tag <CALC ... />
* this will accept any arbitrary string and, if markers of the format <CALC ... />
* are found, will create a new string by substituting in the results of simple
* calculations based on previous numeric answers.
* For example, if Question Q1 asked how many times a person had sex with a female
* and Question Q2 asked how many times a person had sex with a male
* a later question might be "Of the <CALC Q1+Q2 /> times you had sex last week, how many
* times did you also shoot up heroin?"
* The pattern for embedded calculations is <CALC ... />
* This is a static function in anticipation of cases where it has to be used on strings
* not immediately associated with this question.
* @param strInput - the original string to (possibly) alter
* @param interviewId - needed to look up variable values ( answers to previous questions )
* @param iType - needed to look up variable values ( answers to previous questions )
* @param studyId - needed to look up variable values ( answers to previous questions )
* @param listOfAlters - needed to look up variable values ( answers to previous questions )
* @return strInput, but with any <CALC /> tags replaced by the appropriate calculation
*/
public static String calculationInsertion (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
Interview currentInterview;
String strResult = "";
String pattern = "<CALC.*?/>";
String str;
String strExpression;
String strExpressionValue;
ArrayList<String> theList = null;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList(strInput, pattern);
if ( theList==null )
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replace the
// question names between the <VAR /> markers with the answer from
// that question
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strExpression = trimPrefixAndSuffix(str, "<CALC", "/>");
if ( strExpression!=null) {
strExpressionValue = calculateSimpleExpression(strExpression, interviewId, iType, studyId, listOfAlters );
if ( strExpressionValue==null || strExpressionValue.length()==0 )
strExpressionValue = " (CALC error) ";
strResult += " " +strExpressionValue + " ";
} else {
strResult += str;
}
}
return(strResult);
}
/**
* calculateSimpleExpression
* given a string of the format Q1+Q2-Q4 where Q1, Q2 and Q4 are answers to
* previously asked numeric answer questions, calculates the results and returns
* it as a string
* USES LEFT-TO-RIGHT 'CALCULATOR' PRECEDENCE NOT ALGEBRAIC
* @param strInput - string containing a simple mathematical expression such as
* "Q1 + Q2"
* TODO: combine with similar parsing function SimpleLogicMgr.parseComparisonList ???
* @param interviewId - needed to lookup variables (answers to previous questions)
* @param iType - needed to lookup variables (answers to previous questions)
* @param studyId - needed to lookup variables (answers to previous questions)
* @param listOfAlters - needed to lookup variables (answers to previous questions)
* @return the arithmetic result of the expression in string form
*/
private static String calculateSimpleExpression (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
Interview currentInterview;
ArrayList<String> theList = new ArrayList<String>();
String strReturn = "";
String strNextNumber;
boolean bConvertedOkay = true;
MathOp mathOp = MathOp.ADD;
int iTemp;
int iResult = 0;
// First, check for special cases
if ( strInput==null || strInput.length()==0)
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseCalculationList(strInput);
if ( theList.isEmpty())
return(strInput);
iResult = 0;
for ( String str : theList ) {
// System.out.println ( "calc " + str);
if ( str.equals("+")) {
mathOp = MathOp.ADD;
} else if ( str.equals("-")) {
mathOp = MathOp.SUB;
} else if ( str.equals("/")) {
mathOp = MathOp.DIV;
} else if ( str.equals("*")) {
mathOp = MathOp.MUL;
} else {
// first, attempt to treat str as a literal integer value
// if the parse fails assume it is a question title.
// ( question titles are our variables )
try {
iTemp = Integer.parseInt(str);
} catch ( NumberFormatException e ) { // this catch is actually the normal flow
strNextNumber = answerToQuestion(str, interviewId, iType, studyId, listOfAlters );
// System.out.println ( "next number=" + strNextNumber + " " + str + " " + mathOp);
if ( !bConvertedOkay) {
strReturn += "?"+str+"? ";
iTemp = 1;
} else {
try {
iTemp = Integer.parseInt(strNextNumber);
} catch ( NumberFormatException nfe) {
bConvertedOkay = false;
iTemp = 1;
}
}
}
switch ( mathOp ) {
case ADD: iResult += iTemp; break;
case SUB: iResult -= iTemp; break;
case MUL: iResult *= iTemp; break;
case DIV:
// special check for divide-by-zero
if ( iTemp==0 )
return (" (Divide By Zero Error) ");
iResult /= iTemp;
break;
}
}
}
if ( bConvertedOkay ) {
strReturn = Integer.toString(iResult);
}
return(strReturn);
}
/**
* removes leading/trailing spaces and quotes from a string
* @param str the string to really trim
* @return str without quotes
*/
public static String trimQuotes ( String str ) {
str = str.trim();
if ( str.startsWith("\""))
str = str.substring(1);
if ( str.endsWith("\""))
str = str.substring(0,str.length()-1);
str = str.trim();
return(str);
}
/**
* answerCountInsertion
* used with tags of the type <COUNT ... ... />
* this will accept any arbitrary string and, if markers of the format
* <COUNT Question Answer />
* are found, will create a new string by substituting in the number of
* times Answer was given to Question
* For example, if Question Q_RELATION in the ALTER section asked the relationship
* between the ego and the alters a later question might as
* "Of the <COUNT Q_RELATION "brother"> brothers you list, how many are alive?"
* The pattern for embedded calculations is <CALC ... />
* This is a static function in anticipation of cases where it has to be used on strings
* not immediately associated with this question.
* Note that is function does not need as much information as other functions that
* need to look up answers to perform the text substitution, as the COUNT tags
* will need to examine ALL answers to a given question, not just the answer given
* by one alter or an alter pair
* @param strInput - the full prompt to examine
* @param interviewId - needed to look up answers
* @param studyId - needed to look up answers
* @return a new version of strInput with tags <COUNT .../> replaced with appropriate values
*/
public static String answerCountInsertion (String strInput,
Long interviewId, Long studyId) {
Interview currentInterview;
String strResult = "";
String pattern = "<COUNT.*?/>";
String str;
String strExpression;
String strQuestionTitle;
String strAnswerToCount;
String strCountValue;
ArrayList<String> theList = null;
int firstSpace;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList( strInput, pattern);
if ( theList==null )
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replace the
// question names between the <VAR /> markers with the answer from
// that question
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strExpression = trimPrefixAndSuffix(str, "<COUNT", "/>");
if ( strExpression != null ) {
firstSpace = strExpression.indexOf(" "); // split strExpression into question title and answer
if ( firstSpace <= 0 ) {
strResult += "[ERROR " + strExpression + " COUNT needs question and answer]";
} else {
// flips and twists to deal with whitespace and (optional) quotes
strQuestionTitle = strExpression.substring(0,firstSpace).trim();
strAnswerToCount = strExpression.substring(firstSpace).trim();
strQuestionTitle = trimQuotes(strQuestionTitle);
strAnswerToCount = trimQuotes(strAnswerToCount);
if ( strQuestionTitle.length()==0 || strAnswerToCount.length()==0 ) {
strResult += "[ERROR " + strExpression + " COUNT needs Question and Answer]";
} else {
strCountValue = getAnswerCountToQuestion(strQuestionTitle, strAnswerToCount, interviewId, studyId );
strResult += " " + strCountValue + " ";
}
}
} else {
strResult += str;
}
}
return(strResult);
}
/**
* scan the input string for tags of the format <CONTAINS question "answer" /> and
* replaces the text with the number of times this alter ( or alter-pair, or ego )
* answered the question with the answer. The result should be zero or one.
* This will mostly be used in "Use If" expressions.
* @param strInput
* @param interviewId
* @param iType
* @param studyId
* @param listOfAlters
* @return
*/
public static String questionContainsAnswerInsertion (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters) {
Interview currentInterview;
String strResult = "";
String pattern = "<CONTAINS.*?/>";
String str;
String strExpression;
String strQuestionTitle;
String strAnswerToCount;
String strCountValue;
ArrayList<String> theList = null;
int firstSpace;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList( strInput, pattern);
if ( theList==null )
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replace the
// question names between the <VAR /> markers with the answer from
// that question
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strExpression = trimPrefixAndSuffix(str, "<CONTAINS", "/>");
if ( strExpression != null ) {
firstSpace = strExpression.indexOf(" "); // split strExpression into question title and answer
if ( firstSpace <= 0 ) {
strResult += "[ERROR " + strExpression + " CONTAINS needs question and answer]";
} else {
// flips and twists to deal with whitespace and (optional) quotes
strQuestionTitle = strExpression.substring(0,firstSpace).trim();
strAnswerToCount = strExpression.substring(firstSpace).trim();
strQuestionTitle = trimQuotes(strQuestionTitle);
strAnswerToCount = trimQuotes(strAnswerToCount);
// System.out.println ( "question=" + strQuestionTitle);
// System.out.println ( " answer=" + strAnswerToCount);
if ( strQuestionTitle.length()==0 || strAnswerToCount.length()==0 ) {
strResult += "[ERROR " + strExpression + " CONTAINS needs Question and Answer]";
} else {
strCountValue = getQuestionContains(strQuestionTitle, strAnswerToCount,
interviewId, iType, studyId, listOfAlters );
strResult += " " + strCountValue + " ";
}
}
} else {
strResult += str;
}
}
return(strResult);
}
/**
* used to find how many of a given answer where given in response to a specific
* question. This is used only in the ALTER section. For example, if a question
* uses a selection of Male or Female for each alter, a prompt could include this:
* "Of the <COUNT alter_sex "male"/> men you had sex with..."
* @param strQuestionTitle identifies the question (alter_sex in example)
* @param strSurveyAnswer answer to count ( "male" in example )
* @param interviewId - needed for query
* @param studyId - needed for query
* @return - a count of the times this answer was given to this question, -1 on error
*/
public static String getAnswerCountToQuestion ( String strQuestionTitle, String strSurveyAnswer,
Long interviewId, Long studyId ) {
List<Alter> listOfAlters = Alters.getForInterview(interviewId);
ArrayList<Alter> alterPair;
Interview currentInterview;
QuestionOption answerOption;
List<QuestionOption> optionsList = null;
Question question = null;
Answer theAnswer = null;
String strAnswer = strQuestionTitle;
Long iOptionId;
int iCount = 0;
if ( interviewId==null )
return(strQuestionTitle);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strQuestionTitle);
strSurveyAnswer = strSurveyAnswer.trim();
question = Questions.getQuestionUsingTitleAndTypeAndStudy (strQuestionTitle, QuestionType.ALTER, studyId);
if ( question==null )
return ("[ question " + strQuestionTitle + " not found]");
currentInterview = Interviews.getInterview(interviewId);
optionsList = Options.getOptionsForQuestion(question.getId());
for ( Alter alter : listOfAlters ) {
alterPair = new ArrayList<Alter>();
alterPair.add(alter);
theAnswer = Answers.getAnswerForInterviewQuestionAlters( currentInterview, question, alterPair);
if ( theAnswer!=null ) {
switch ( theAnswer.getAnswerType()) {
case SELECTION:
case MULTIPLE_SELECTION:
strAnswer = "";
// the answer is a list of comma separated ids
// that is, a list of optionIDs
for ( String strOption : theAnswer.getValue().split(",")) {
try {
iOptionId = Long.parseLong(strOption);
} catch ( NumberFormatException nfe ) {
iOptionId = -1L;
}
// Now find the option that this ID refers to
answerOption = null;
for ( QuestionOption qo : optionsList ) {
if ( qo.getId().equals(iOptionId)) {
answerOption = qo;
}
}
// lastly, if this answer matches the answer we
// are counting up increment our count
if ((answerOption != null) && strSurveyAnswer.equalsIgnoreCase(answerOption.getName().trim()))
++iCount;
}
break;
case TEXTUAL:
case NUMERICAL:
strAnswer = theAnswer.getValue().trim();
if ( strSurveyAnswer.equals(strAnswer))
++iCount;
break;
}
}
}
return(Integer.toString(iCount));
}
/**
* this is similar to getAnswerCountToQuestion above, but tests to see if
* the answer to a question contains a string for just one alter ( or alter pair)
* this is so the <CONTAINS logic can be used for skipping questions
* @param strQuestionTitle
* @param strSurveyAnswer
* @param interviewId
* @param iType
* @param studyId
* @param listOfAlters
* @return
*/
public static String getQuestionContains ( String strQuestionTitle, String strSurveyAnswer,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
Interview currentInterview;
QuestionOption answerOption;
List<QuestionOption> optionsList = null;
Question question = null;
Answer theAnswer = null;
String strAnswer = strQuestionTitle;
String userAnswer;
String[] answersToLookFor;
Long iOptionId;
int iCount = 0;
if ( interviewId==null )
return(strQuestionTitle);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strQuestionTitle);
answersToLookFor = strSurveyAnswer.trim().split(":");
question = Questions.getQuestionUsingTitleAndTypeAndStudy (strQuestionTitle, iType, studyId);
if ( question==null )
return ("[ " + strQuestionTitle + " not found]");
currentInterview = Interviews.getInterview(interviewId);
optionsList = Options.getOptionsForQuestion(question.getId());
theAnswer = Answers.getAnswerForInterviewQuestionAlters( currentInterview, question, listOfAlters);
if ( theAnswer!=null ) {
switch ( theAnswer.getAnswerType()) {
case SELECTION:
case MULTIPLE_SELECTION:
strAnswer = "";
// the answer is a list of comma separated ids
// that is, a list of optionIDs
for ( String strOption : theAnswer.getValue().split(",")) {
try {
iOptionId = Long.parseLong(strOption);
} catch ( NumberFormatException nfe ) {
iOptionId = -1L;
}
// Now find the option that this ID refers to
answerOption = null;
for ( QuestionOption qo : optionsList ) {
if ( qo.getId().equals(iOptionId))
answerOption = qo;
}
// lastly, if this answer matches the answer we
// are counting up increment our count
// if ((answerOption != null) && strSurveyAnswer.equalsIgnoreCase(answerOption.getName().trim()))
// ++iCount;
// }
if ( answerOption != null ) {
userAnswer = answerOption.getName().trim();
for ( String str : answersToLookFor ) {
if ( str.equalsIgnoreCase(userAnswer))
++iCount;
}
}
}
break;
case TEXTUAL:
case NUMERICAL:
strAnswer = theAnswer.getValue().trim();
for ( String str : answersToLookFor ) {
if ( str.equals(strAnswer))
++iCount;
}
break;
}
}
return(Integer.toString(iCount)); // Should be zero or one
}
/**
* conditionalTextInsertion
* this will accept any arbitrary string and, if markers of the format <VAR ... />
* are found, will create a new string by substituting in answers.
* For example, if Question Q1 asked how many times last week a person smoked crack,
* a later question might be "Of the <VAR Q1 /> times you smoked crack last week, how many
* times did you also drink?"
* The pattern for embedded variables is <VAR ... />
* This is a static function in anticipation of cases where it has to be used on strings
* not immediately associated with this question.
* @param strInput - the original string to (possibly) alter
* @param interviewId - needed to look up variable values ( answers to previous questions )
* @param iType - needed to look up variable values ( answers to previous questions )
* @param studyId - needed to look up variable values ( answers to previous questions )
* @param listOfAlters - needed to look up variable values ( answers to previous questions )
* @return strInput, but with any <IF /> tags either removed or replaced by text
*/
public static String conditionalTextInsertion (String strInput,
Long interviewId, Question.QuestionType iType, Long studyId, ArrayList<Alter> listOfAlters ) {
Interview currentInterview;
String strResult = "";
String pattern = "<IF.*?/>";
String strContents;
String strExpression;
String strText;
String str;
ArrayList<String> theList;
int iExpressionResult;
int iLastQuote;
int iFirstQuote;
int ix;
// if no interviewId we are previewing the question
// return original prompt unaltered
if ( interviewId==null )
return(strInput);
currentInterview = Interviews.getInterview(interviewId);
if ( currentInterview==null || currentInterview.getStudyId()==null )
return(strInput);
theList = parseExpressionList ( strInput, pattern);
if (theList==null)
return(strInput);
// At this point we have an array list with literal strings
// alternating with variable markers .
// now construct the output string by replace the
// question names between the <VAR /> markers with the answer from
// that question
for ( ix=0 ; ix<theList.size(); ++ix ) {
str = theList.get(ix);
strContents = trimPrefixAndSuffix ( str, "<IF", "/>");
if ( strContents!=null ) {
iLastQuote = strContents.lastIndexOf("\"");
iFirstQuote = strContents.lastIndexOf("\"", iLastQuote-1);
// System.out.println ( "quotes=" + iFirstQuote + "," + iLastQuote);
if ( iLastQuote<0 || iFirstQuote<0) {
System.out.println ("ERROR Problem in TextInsertionUtil.conditionalTextInsertion, missing quotes");
System.out.println ("Returning " + strResult);
return(strResult);
}
strExpression = strContents.substring(0,iFirstQuote);
strText = strContents.substring(iFirstQuote+1, iLastQuote);
// System.out.println ( "strExpression=#" + strExpression + "#");
// System.out.println ("strText=#" + strText + "#");
iExpressionResult = SimpleLogicMgr.createSimpleExpressionAndEvaluate (
strExpression, interviewId, iType, studyId, listOfAlters );
if ( iExpressionResult != 0 )
strResult += " " + strText + " ";
} else {
strResult += str;
}
}
return(strResult);
}
/**
* converts a string to an Arraylist of strings that alternate between those
* matching a regular expression and those that don't
* @param strInput the original string to alter
* @param strRegExp the regular expression describing the pattern to search for
* @return an ArrayList<String> with alternating strings matching the pattern and not
*/
private static ArrayList<String> parseExpressionList ( String strInput, String strRegExp) {
Pattern p = Pattern.compile(strRegExp, Pattern.CASE_INSENSITIVE );
Matcher matcher = p.matcher(strInput);
ArrayList<String> theList = null;
boolean found = false;
int iVarCount = 0;
int iStartIndex = 0;
int iVarStart;
int iVarEnd;
// First, check for special cases
if ( strInput==null || strInput.length()==0)
return(theList);
// another special check
found = matcher.find(iStartIndex);
if ( !found )
return(theList);
theList = new ArrayList<String>();
// Second, split the input string into substrings, which will be
// literal portions and variable portions ( <VAR.../> ) (or the strRegExp)
while ( found ) {
found = matcher.find(iStartIndex);
if ( found ) {
++iVarCount;
iVarStart = matcher.start();
iVarEnd = matcher.end();
if ( iVarStart>iStartIndex )
theList.add( strInput.substring(iStartIndex,iVarStart));
theList.add(strInput.substring(iVarStart,iVarEnd));
iStartIndex = iVarEnd;
} else {
theList.add(strInput.substring(iStartIndex));
}
}
return(theList);
}
/**
* IF a string starts with the prefix and ends with the suffix this extracts the middle
* and returns it.
* This is useful in the above functions that need to look for and deal with specific
* tags 'enclosing' things like question titles, <VAR Q1 /> et al
* @param strInput the original string to 'tear apart'
* @param strPrefix the starting string
* @param strSuffix the ending string
* @return null if the prefix AND suffix are not present, otherwise
* the middle portion minus these
*/
private static String trimPrefixAndSuffix ( String strInput, String strPrefix, String strSuffix ) {
String strReturn = null;
int preLen;
int sufLen;
if ( strInput.startsWith(strPrefix) && strInput.endsWith(strSuffix)) {
preLen = strPrefix.length();
sufLen = strSuffix.length();
strReturn = strInput.substring(preLen, strInput.length()-sufLen).trim(); // extract question title
}
return(strReturn);
}
/**
* creates an arrayList of strings from a larger input string
* in anticipation of (rather simple) expression calculation.
* For our purposes the calculations will only involve variables
* separated by the operands +,-,*,/
* @param strInput string of the form Q1+Q2-4
* @return Arraylist of the form "q1","+","Q2","-","4"
*/
private static ArrayList<String> parseCalculationList ( String strInput ) {
// now create an array of strings alternating between "-" and "+"
// and the text segments between them. Have to do this the hard way
ArrayList<String> theList = new ArrayList<String>();
int iInputLength;
int iWordStart;
int iWordEnd;
char ch;
ch = ' ';
iWordStart = iWordEnd = 0;
iInputLength = strInput.length();
for ( iWordEnd=0 ; iWordEnd<iInputLength; ++iWordEnd ) {
ch = strInput.charAt(iWordEnd);
if ( ch=='+' || ch=='-' || ch=='/' || ch=='*') {
if ( iWordEnd>iWordStart+1)
theList.add(strInput.substring(iWordStart,iWordEnd).trim());
theList.add(new String(new char[]{ch}));
iWordStart = iWordEnd+1;
}
}
// pick up the trailing word
if ( iWordStart < iInputLength )
theList.add(strInput.substring(iWordStart).trim());
return(theList);
}
/**
* simple little function to get the index of the month.
* compare just the first three letters to avoid confusion with
* misspellings and whether or not abreviations have a period.
* also allow for 1 based indexs
* @param str a string which (should be) a month
* @return one based index
*/
private static int getMonthIndex ( String str ) {
int ix;
int iValue;
if (str==null || str.length()==0)
return(-1);
str = str.trim();
if ( str.length()>3 )
str = str.substring(0,3);
for ( ix=0 ; ix<strMonths.length ; ++ix ) {
if ( str.equalsIgnoreCase(strMonths[ix].substring(0,3)))
return(ix);
}
// if the month value does not match one of the three letter
// abreviations it may be the 1 based index of the month
try {
iValue = Integer.parseInt(str);
} catch ( NumberFormatException nfe ) {
return(-1);
}
while ( iValue < 1 )
iValue += 12;
while ( iValue>12 )
iValue -= 12;
return(iValue-1);
}
/**
* a simple convenience function to convert a date in string format
* to three integers. assumes the date is of the format
* MMM DD YYYY
* where YYY and DD are already integers and the month MM
* is either an integer or a string
* @param strDate date in format "Mar 27 1958"
* @return array int[3] = {3, 27, 1958};
*/
private static int[] strDateToNumeric ( String strDate) {
String[] dateInfo;
int[] returnInfo = new int[3];
returnInfo[0] = returnInfo[1] = returnInfo[2] = 0;
dateInfo = strDate.split(" ");
switch ( dateInfo.length ) {
default:
case 3:
try {
returnInfo[2] = Integer.parseInt(dateInfo[2].trim());
} catch ( NumberFormatException nfe3 ) {
returnInfo[2] = 0;
}
// fall thru
case 2:
try {
returnInfo[1] = Integer.parseInt(dateInfo[1].trim());
} catch ( NumberFormatException nfe1 ) {
returnInfo[1] = 0;
}
// fall thru
case 1:
returnInfo[0] = getMonthIndex(dateInfo[0].trim());
// if the month is out of bounds most likely
// there was an error in constructing the <DATE tag
if ( returnInfo[0]<0 ) {
System.out.println ( "strDate=" + strDate);
System.out.println ( "returnInfo[0]=" + returnInfo[0]);
returnInfo[0] = 0;
}
if ( returnInfo[0]>11 ) {
System.out.println ( "strDate=" + strDate);
System.out.println ( "returnInfo[0]=" + returnInfo[0]);
returnInfo[0] = 11;
}
}
return(returnInfo);
}
/**
* applys an offset to the appropriate field of a date represented by an
* array of 3 ints.
* @param date 'starting point' date
* @param dateField the field in increment/decrement
* @param offset amount to add/subtract from the specified field
* @return a new array of three ints representing a date
*/
private static int[] applyOffsetToDate ( int[] date, DATE_FIELD dateField, int offset ) {
int returnArray[] = new int[3];
int Month = DATE_FIELD.MONTH.ordinal();
int Day = DATE_FIELD.DAY.ordinal();
int Year = DATE_FIELD.YEAR.ordinal();
returnArray[Month] = date[Month];
returnArray[Day] = date[Day];
returnArray[Year] = date[Year];
switch ( dateField ) {
case NONE:
break;
case DAY:
returnArray[Day] += offset;
while ( returnArray[Day]<1 ) {
returnArray[Day] += 31;
--returnArray[Month];
if ( returnArray[Month] < 0 ) {
returnArray[Month] = 11;
--returnArray[Year];
}
}
while ( returnArray[Day]>31 ) {
returnArray[Day] -= 31;
++returnArray[Month];
if ( returnArray[Month] > 11 ) {
returnArray[Month] = 0;
++returnArray[Year];
}
}
break;
case MONTH:
returnArray[Month] += offset;
while ( returnArray[Month] < 0 ) {
returnArray[Month] += 12;
--returnArray[Year];
}
while ( returnArray[Month] > 11 ) {
returnArray[Month] -= 12;
++returnArray[Year];
}
break;
case YEAR:
returnArray[Year] += offset;
break;
}
if ( returnArray[Day] > monthCaps[returnArray[Month]])
returnArray[Day] = monthCaps[returnArray[Month]];
return(returnArray);
}
/**
* this will be for 'preview' panels, where we need to display the
* text of a question and its variable insertion tags as-is but
* escaped so they are not interpreted by the HTML functions.
* Also, this will do *some* syntax checking
* @param strInput string to escape
* @return same string, but escaped
*/
public static String escapeTextInsertionTags ( String strInput ) {
String[] pattern = {
"<VAR.*?/>", "<DATE.*?/>", "<CALC.*?/>", "<COUNT.*?/>", "<CONTAINS.*?/>", "<IF.*?/>"};
String[] strStartTag = {
"<VAR", "<DATE", "<CALC", "<COUNT", "<CONTAINS", "<IF"};
String[] strStartTagReplacement = {
"<VAR ", "<DATE ", "<CALC ", "<COUNT ", "<CONTAINS ", "<IF "};
String strEndTag = "/>";
String strEndTagReplacement = " />";
String innerString;
String newString = "";
ArrayList<String> theList;
int ix;
if ( strInput==null || strInput.length()==0)
return(strInput);
for ( ix=0 ; ix<pattern.length ; ++ix ) {
theList = parseExpressionList ( strInput, pattern[ix]);
if ( theList != null ) {
newString = "";
for ( String string : theList ) {
if ( string.startsWith(strStartTag[ix])) {
innerString = trimPrefixAndSuffix ( string, strStartTag[ix], strEndTag);
innerString = tagSpecificVerification (innerString, ix);
newString += strStartTagReplacement[ix] + innerString + strEndTagReplacement;
} else {
newString += string;
}
}
strInput = newString;
}
}
return(strInput);
}
/**
* will do (simple) verification of the syntax of specific tags
* @param str string within the start and stop tags ( <VAR ... />
* @param iTagIndex an integer indicating which tag we are verifying.
* look to the inner variables of escapeTextInsertionTags and the
* switch cases statements for explanation
* @return the original string with (optional) warning messages appended.
*/
private static String tagSpecificVerification (String str, int iTagIndex) {
String strReturn = str;
String[] innerList;
int innerWordCount;
innerList = str.trim().split(" ");
innerWordCount = innerList.length;
if (innerWordCount<1 ) {
strReturn += " NO PARAMETER ";
} else if (innerWordCount==1 && innerList[0].trim().length()==0 ) {
strReturn += " EMPTY ";
} else {
switch ( iTagIndex ) {
case 0: // VAR
if ( innerWordCount>1 ) {
strReturn += " TOO MANY PARAMETERS ";
}
break;
case 1: // DATE
if ( innerWordCount>3 ) {
strReturn += " TOO MANY PARAMETERS ";
}
break;
case 2: // CALC
if ( innerWordCount>3 ) {
strReturn += " TOO MANY PARAMTERS ";
}
break;
case 3: // COUNT
break;
case 4: // CONTAINS
break;
case 5: // IF
break;
default:
break;
}
}
return(strReturn);
}
}