/* * MDateInvestigator.java * * Created on October 13, 2002, 2:26 PM */ import java.text.DateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.text.ParsePosition; /** * Methods to investigate a date, as per the requirements of Project 1 * The description of Project 1 can be found at: * http://www.cs.washington.edu/education/courses/143/02au/projects/p1/instructions.html * The investigator processes dates of certain valid formats * and determines which dates are "interesting", * based both on the full date, and on the year only. * The base date for "interestingness" is October 31, 2002. * * @author dickey */ public class MDateInvestigator implements IDateInvestigator { /** smallest & largest required radix the program is required to check */ public static final int STARTING_RADIX = 2; public static final int ENDING_RADIX = 20; private GregorianCalendar enteredDate; //will contain the date typed in private GregorianCalendar halloweenDate = new GregorianCalendar(2002, GregorianCalendar.OCTOBER, 31); private String theInformation; /** Creates a new instance of MDateInvestigator */ public MDateInvestigator() { super(); //superclass constructor, if any theInformation = "no information yet"; } /**These constructors are unncessary for Project 1 */ /** "userMonth" is a month in the range 1-12 * (as opposed to the Calendar convention of 0-11) */ public MDateInvestigator(int year, int userMonth, int day) { this(new GregorianCalendar(year, userMonth-1, day)); } public MDateInvestigator (GregorianCalendar gCal) { this(); //First, get the right kind of formatter DateFormat dF = DateFormat.getDateInstance(DateFormat.LONG); String dateString = dF.format(gCal.getTime()); boolean thrownAway = this.setDate(dateString); } public MDateInvestigator(String dateAsString) { this(); this.setDate(dateAsString); } /** get a string with complete information about the date. * The definition of "complete information" may depend upon the Investigator. * However, for valid dates, all of the information of "interestingness" must * be concatenated into this one string. * If the string is long, it should contain line feeds at appropriate places. * The method should never return null. * Suggestion: if the date is invalid, return a string that explains why. * * In the present implementation, the information is computed and saved * when the date is set. */ public String dateInformation() { return theInformation; } /** Return a string identifying this date investigator's author. * The string should be short. */ public String getAuthor() { return "Martin"; } /** Return a string which names or briefly describes the date investigator class. * The string should be short. */ public String getDescription() { return "sample solution date investigator"; } /** set the date to be investigated. * returns true if the date is valid, false otherwise. * The definition of "valid" may depend on the particular Investigator * * For project 1, the required valid date string formats are: * * , <4 digit year num> * //<4 digit year num> * * As it happens, these formats are covered by the LONG and SHORT formats * of DateFormat */ public boolean setDate(String dateString) { if (dateString == null) { return false; } //Date trialDate = null; this.enteredDate = tryToParseDateString(dateString); if (this.enteredDate == null) { //no luck this.theInformation = "Most recent date string " + dateString + " was invalid."; return false; } if (this.halloweenDate.getTime().compareTo(this.enteredDate.getTime()) < 0) { this.theInformation = "Invalid date string " + dateString + ": is after " + "Halloween"; return false; } //finally, we have a good date //Get it in nice printable formats String resultingStringDate = displayDateFormats(this.enteredDate); this.theInformation = "\n" + resultingStringDate; //Find out now about its interestingness, and save. //Will be no need to do again. this.theInformation += "\n" + checkDateInterestingness(this.enteredDate, this.halloweenDate); return true; } //end setDate /** See if the date is "interesting" in the sense of Project 1. * That is, see if the number of years or number of dates that have * elapsed (to the current date, Halloween), is interesting. * Return a string with the results */ public static String checkDateInterestingness(GregorianCalendar checkDate, GregorianCalendar halloween) { int daysElapsed = daysElapsed(checkDate, halloween); String daysI = checkDaysElapsedInterestingness(daysElapsed); int yearsElapsed = yearsElapsed(checkDate, halloween); String yearsI = checkYearsInterestingness(yearsElapsed); String message = yearsElapsed + " years have elapsed -- "; if (yearsI == null) { message += " an uninteresting number."; } else { message += " an interesting number because it is:\n\t" + yearsI; } message += "\n" + daysElapsed + " days have elapsed -- "; if (daysI == null) { message += " an uninteresting number."; } else { message += " an interesting number because it is:\n\t" + daysI; } return message; } /** See if the full date is interesting, that is, if the number of days *from then until now is interesting. *If interesting, return a string explaining why. *If not interesting, return null */ public static String checkDaysElapsedInterestingness(int elapsedDays) { String result = ""; for (int radix = STARTING_RADIX; radix <= ENDING_RADIX; radix++) { int minNeededZeros = minDayTrailingZerosOfInterest (radix); String iResult = checkNumberInterestingness(elapsedDays, radix, minNeededZeros); if (iResult != null) { result += iResult; } } if (result.equals("")) { return null; } else { return result; } } //end checkDaysElapsedInterestingness /** find out how many days have elapsed between two Gregorian dates */ public static int daysElapsed(GregorianCalendar fromDate, GregorianCalendar toDate) { long fromMillis = fromDate.getTimeInMillis(); long toMillis = toDate.getTimeInMillis(); long elapsedDays = (toMillis - fromMillis)/(1000*60*60*24); assert elapsedDays < Integer.MAX_VALUE && elapsedDays > Integer.MIN_VALUE; return (int) elapsedDays; } //end daysElapsed public static int yearsElapsed(Calendar fromDate, Calendar toDate) { int fromYear = fromDate.get(Calendar.YEAR); int toYear = toDate.get(Calendar.YEAR); return toYear - fromYear; } /*Given a radix, how many trailing zeros must a number in that base have * to be considered interesting? * At present, these values come from the problem statement */ private static int minYearTrailingZerosOfInterest(int radix) { if (radix <= 4) { return 4; } else { return 2; } } //minZerosOfInterest private static int minDayTrailingZerosOfInterest(int radix) { if (radix <= 4) { return 8; } else { return 4; } } //minZerosOfInterest private static String checkYearsInterestingness(int yearsPassed) { String results = ""; for (int radix = STARTING_RADIX; radix <= ENDING_RADIX; radix++) { int minNeededZeros = minYearTrailingZerosOfInterest (radix); String iResults = checkNumberInterestingness(yearsPassed, radix, minNeededZeros); if (iResults != null) { results += iResults; } } if (results.equals("")) { return null; } else { return results; } } //checkYearOnlyInterestingness /** If the number is interesting (ends in at least as many zeros as needed), *return a message to that effect. *Otherwise, return null */ public static String checkNumberInterestingness(long numToCheck, int radix, int minNeededZeros) { String result = ""; int actualZerosCount = countTrailingZeros(numToCheck, radix); if (actualZerosCount >= minNeededZeros) { //this number ends in at least minNeededZeros, base "radix" String strNum = Integer.toString((int) numToCheck, radix); result += " " + strNum + " (base " + radix + ")"; } if (result.equals("")) { return null; } else { return result; } } //end checkNumberInterestingness public static int countTrailingZeros(long numToCheck, int radix) { int count = 0; while (numToCheck > 0 && //if division by the radix has a remainder of 0, //it means the rightmost digit is 0 in that base numToCheck % radix == 0) { count++; numToCheck = numToCheck / radix; //discard the rightmost digit } return count; } //end countTrailing Zeros /** Try to parse the string as a date, in the allowed formats. * return null if the string cannot be parsed */ private static GregorianCalendar homeBrewedDateParse(String dateString) { GregorianCalendar g = null; g = homeBrewedParseLongFormat(dateString); if (g == null) { g = homeBrewedParseShortFormat(dateString); } if (g != null) { //just for debug //System.out.println("home brewed parse\n\t" + g); } return g; //might be null } //homeBrewed /** try to parse the string as if it held dates of form * , <4-digit year num> * E.g.: November 3, 1961 */ private static GregorianCalendar homeBrewedParseLongFormat(String dateString) { dateString = dateString.trim(); int spacePos = dateString.indexOf(" "); if (spacePos < 0) {return null;} //parse fails String monthStr = dateString.substring(0, spacePos); int monthnum = monthStringToNum(monthStr); if (monthnum < 0) {return null;} //wasn't a valid month int commaPos = dateString.indexOf(','); if (commaPos < 0) {return null;} //the two characters (at most) before the comma are the day String dayStr = dateString.substring(spacePos+1, commaPos); int daynum = dayStrToDayNum(dayStr); if (daynum < 0) {return null;} //characters after the comma must be the year String yearStr = dateString.substring(commaPos+1); int yearnum = yearStrToNum(yearStr); if (yearnum < 0) { return null;} //The year is good. Now check the day number int maxDays = daysInMonth(monthnum, yearnum); if (daynum > maxDays) {return null;} return new GregorianCalendar(yearnum, monthnum, daynum); } //end parse long date format /**Given a string, see if its 1st three characters match the first *three characters of an English month name (case insensitive) *Return 0 to 11 if found, -1 if not found. */ public static int monthStringToNum(String monthString) { final String[] monthnames = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; monthString = monthString.trim(); if (monthString.length() < 3) {return -1;}; String first3 = monthString.substring(0,3); for (int m = 0; m < monthnames.length; m++) { if (first3.equalsIgnoreCase(monthnames[m].substring(0,3))) { return m; } } //end for return -1; }//end monthStringToNum /** Determine number of days in the given month. *Preconditions: 0 <= monthnum < 12; yearnum > 0 */ public static int daysInMonth(int monthnum, int yearnum) { final int[] maxDaysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; assert 0 <= monthnum && monthnum < 12; assert yearnum > 0; if (monthnum != 1) { return maxDaysInMonth[monthnum]; } //February if (isLeapYear(yearnum)) { return 29; } else { return 28; } } //end daysInMonth public static boolean isLeapYear(int yearnum) { assert yearnum > 0; if (yearnum % 4 != 0) { return false; } // year is divisible by 4 if (yearnum % 1000 == 0) { return true; } if (yearnum % 400 == 0) { return false; } return true; } //end isLeapYear /** try to parse a string as if it held dates of form * //<4-digit year> * E.g.: 11/3/1961 */ private static GregorianCalendar homeBrewedParseShortFormat(String dateString) { int slash1 = dateString.indexOf('/'); if (slash1 < 0) {return null;} String monthUserNumStr = dateString.substring(0,slash1).trim(); int monthnum = monthUserNumStrToMonthNum(monthUserNumStr); if (monthnum < 0) {return null;} int slash2 = dateString.indexOf('/', slash1+1); if (slash2 < 0) {return null;} String dayStr = dateString.substring(slash1+1, slash2).trim(); int daynum = dayStrToDayNum(dayStr); if (daynum < 0) {return null;} String yearStr = dateString.substring(slash2+1); int yearnum = yearStrToNum(yearStr); if (yearnum < 0) {return null;} //now we have month, day, and year as numbers if (daynum > daysInMonth(monthnum, yearnum)) {return null;} return new GregorianCalendar(yearnum, monthnum, daynum); } //end parse short date format /** convert a string with a month as users would type it -- 1 to 12 -- * to an internal month index, 0 to 11 * return -1 if conversion fails */ public static int monthUserNumStrToMonthNum(String monthUserNum) { int mnum = strToNum(monthUserNum, 1, 12); if (mnum < 0) { return -1; } return mnum - 1; } /** convert a string with a day of the month * to an integer * return -1 if conversion fails */ public static int dayStrToDayNum(String strNum) { return strToNum(strNum, 1, 31); } /** convert a string with a year * to an integer * return -1 if conversion fails */ public static int yearStrToNum(String strNum) { return strToNum(strNum, 1, 9999 ); } /** convert a string supposedly containing an integer in the range * minval ... maxval * to an integer * return -1 if conversion fails */ public static int strToNum(String strNum, int minval, int maxval) { int num; try { num = Integer.parseInt(strNum.trim()); } catch (NumberFormatException e) { return -1; } if (num >= minval && num <= maxval) { return num; } return -1; } /** return null if the string could not be parsed as a date */ private static GregorianCalendar tryToParseDateString(String dateString) { GregorianCalendar trialDate = null; trialDate = homeBrewedDateParse(dateString); /* *The following is another way to parse the date. *It uses the DateFormat class. *Overall, it knows more formats. *However, the homeBrewedDateParse uses less of the Java libraries * if (trialDate == null) { trialDate = TryToParseDate(dateString, DateFormat.SHORT); } if (trialDate ==null) { trialDate = TryToParseDate(dateString, DateFormat.MEDIUM); if (trialDate ==null) { trialDate = TryToParseDate(dateString, DateFormat.LONG); if (trialDate ==null) { trialDate = TryToParseDate(dateString, DateFormat.FULL); } } } */ return trialDate; } /* Not used at present. * * shows how to use the DateFormat class to parse a date string */ /** return null if the date could not be parsed * The "style" is one of the four styles SHORT MEDIUM LONG FULL */ /* private static GregorianCalendar TryToParseDate(String dateString, int style) { DateFormat dformatter = DateFormat.getDateInstance(style); Date dateToTry = dformatter.parse(dateString, new ParsePosition(0)); //System.out.print(" parse result for " + dateString + // " for style " + style + ": "); if (dateToTry == null) { //System.out.println(" invalid "); //date format was invalid return null; } else { //for debug only System.out.println("\n" + dateString + " is valid! " + " Full value is " + dateToTry.toString()); GregorianCalendar g = new GregorianCalendar(); g.setTime(dateToTry); return g; } } //end TryToParseDate */ private static String displayDateFormats(GregorianCalendar dateToDisplay) { Date theGoodDate = dateToDisplay.getTime(); String result = "All four formats of the date (for the default locale):"; DateFormat dformatSHORT = DateFormat.getDateInstance(DateFormat.SHORT); DateFormat dformatMEDIUM = DateFormat.getDateInstance(DateFormat.MEDIUM); DateFormat dformatLONG = DateFormat.getDateInstance(DateFormat.LONG); DateFormat dformatFULL = DateFormat.getDateInstance(DateFormat.FULL); result = result + "\t" + "\n\t" + dformatSHORT.format(theGoodDate) + "\n\t" + dformatMEDIUM.format(theGoodDate) + "\n\t" + dformatLONG.format(theGoodDate) + "\n\t" + dformatFULL.format(theGoodDate); return result; } //end displayDateFormats /** A little test method */ public static void main(String args[]) { System.out.println(checkYearsInterestingness(256)); System.out.println(checkYearsInterestingness(3*3*3*5*5*4*4)); System.out.println(checkYearsInterestingness(3*3*3*5*5*5*7*7*11*11*4*4*4)); System.out.println(checkYearsInterestingness(3*3*3*5*5*7*7*11*11*4*4*4*4)); System.out.println(checkDaysElapsedInterestingness(256)); System.out.println(checkDaysElapsedInterestingness(3*3*3*5*5*4*4)); System.out.println(checkDaysElapsedInterestingness(3*3*3*5*5*5*7*7*11*11*4*4*4)); System.out.println(checkDaysElapsedInterestingness(3*3*3*5*5*7*7*11*11*4*4*4*4)); IDateInvestigator mi = new MDateInvestigator(); System.out.println(mi.getDescription()); mi.setDate("6/15/1991"); System.out.println(mi.dateInformation()); mi.setDate("1/1/1902"); System.out.println(mi.dateInformation()); mi.setDate("10/15/2002"); System.out.println(mi.dateInformation()); MDateInvestigator mdi = new MDateInvestigator(902, 4, 1); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator(1906, 9, 19); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator(1975, 6, 15); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator(1202, 1, 1); System.out.println(mdi.dateInformation()); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator("October 9, 1990"); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator("jun 15, 1975"); System.out.println(mdi.dateInformation()); mdi = new MDateInvestigator("feb 29, 1904"); System.out.println(mdi.dateInformation()); } } //end class MDateInvestigator