/* * EPROG Aufgabe #1106 (maskdate) * von Hannes Eder, e9521554@stud3.tuwien.ac.at * * Dokumentation siehe: doc/index.html, bzw. maskdate.txt */ import eprog.EprogIO; import java.io.PrintStream; import java.io.StringReader; import java.text.NumberFormat; /** * <b>Eprog Aufgabe #1106</b><br> * Autor: <b><a href="mailto:e9521554@student.tuwien.ac.at">Hannes Eder <e9521554@student.tuwien.ac.at></a></b><br><br> * * siehe auch: <a href="../maskdate.html">Spezifikation</a><br><br> * * Um den Buildprozess zu vereinfachen, wurde ein <a href="../Makefile">Makefile</a> erstellt.<br><br> * * @version 1.0 */ public class maskdate { /** * wird <tt>java maskdate --debug</tt> ausgeführt, ist <tt>debug</tt> true, dadurch wird <tt>test_All</tt> aufgerufen * * @see maskdate#test_All */ private static boolean debug = false; /** läuft irgendetwas falsch, dann wird dieser Text ausgegeben */ final private static String msgFalscheEingabe = "FALSCHE EINGABE"; /** * Main Entry Point. * * Überprüft <tt>args</tt> auf <tt>--debug</tt>, führt geg. falls einen Selbsttest * siehe <tt>test_All</tt> durch. Im Normalfall werden aber nur 2 Wörter über * <tt>EprogIO.readWord</tt> gelesen. <tt>maskDate</tt> * erzeugt den Output. * * @see maskdate#maskDate * @see maskdate#test_All * * @returns <tt>0</tt>kein Fehler bei der Verarbeitung, <tt>-1</tt>Selbsttest gescheitert */ public static void main(String[] args) { int exitCode = 0; for ( int i=0; i < args.length; i++ ) { if ( args[i].equals("--debug") ) debug = true; } if (debug && (!test_All(System.err))) { // Selbsttest gescheitert System.err.println("[ERROR] Selbsttest gescheitert"); exitCode = -1; } else { String date = EprogIO.readWord(); String format = EprogIO.readWord(); String output = maskDate(date, format); EprogIO.println(output); } System.exit(exitCode); } /** * Führt einen Selbsttest der wichtigsten Funktionen durch * @param log <tt>PrintStream</tt> auf dem das log ausgegeben werden soll, im Normalfall <tt>System.err</tt> * @return true, wenn der Test ok war. */ private static boolean test_All(PrintStream log) { boolean bRet = true; bRet &= test_isLeapYear(log); bRet &= test_isDateValid(log); bRet &= test_checkFormat(log); bRet &= test_dayOfWeekOfFirstJanuary(log); bRet &= test_dayOfYear(log); bRet &= test_dayOfWeek(log); bRet &= test_formatDate(log); bRet &= test_maskDate(log); log.println((bRet ? "[OK]": "[ERR]") + " test_All"); return bRet; } /** * @return true, wenn text dem Aufbau von format entspricht, sonst false */ private static boolean checkFormat(String format, String text) { boolean bRet = true; if (format.length() != text.length()) { bRet = false; } else { for (int i = 0; i < format.length(); i++) { char formatChar = format.charAt(i); char textChar = text.charAt(i); if ((formatChar == '#' && !Character.isDigit(textChar)) || (formatChar != '#' && formatChar != textChar) ) { bRet = false; break; } } } return bRet; } /** * Testet checkFormat mit ein paar repraestentativen Werten * @return true, wenn alle Test ok sind */ private static boolean test_checkFormat(PrintStream log) { boolean bRet = true; bRet &= checkFormat("##/##/####", "12/34/5678") == true; bRet &= checkFormat("##-##-####", "99-99-9999") == true; bRet &= checkFormat("##/##/####", "1/2/1976") == false; // die Länge muss auch stimmen! bRet &= checkFormat("##/##/####", "12/12/12345") == false; // detto bRet &= checkFormat("##/##/####", "12.12/1999") == false; bRet &= checkFormat("##/##/####", "##/12/2001") == false; log.println((bRet ? "[OK]": "[ERR]") + " test_checkFormat"); return bRet; } /** * @return true, wenn year, month und day ein gueltiges Datum bilden, year >= 1! */ private static boolean isDateValid(int year, int month, int day) { boolean bRet = true; bRet &= (year >= 1); bRet &= (month >= 1) & (month <= 12); bRet &= (day >= 1) & (day <= 31); switch (month) { case 2: bRet &= (day <= 28 + (isLeapYear(year) ? 1 : 0)); break; case 4: case 6: case 9: case 11: bRet &= (day <= 30); break; } return bRet; } /** * Testet die Funktion isDateValid mit repraestentativen Werten * @return true, wenn alle test ok waren */ private static boolean test_isDateValid(PrintStream log) { boolean bRet = true; bRet &= isDateValid(0, 1, 1) == false; bRet &= isDateValid(1, 0, 1) == false; bRet &= isDateValid(1, 1, 1) == true; bRet &= isDateValid(1, 12, 31) == true; bRet &= isDateValid(1, 12, 32) == false; // wie die Werbung schon sagt, am 32.12 ist es zu spät bRet &= isDateValid(1, 13, 1) == false; bRet &= isDateValid(1976, 1, 2) == true; // mein Geburtstag ist klarerweise ein gueltiges Datum (c= bRet &= isDateValid(2000, 2, 29) == true; bRet &= isDateValid(2001, 2, 29) == false; bRet &= isDateValid(2001, 4, 31) == false; bRet &= isDateValid(2001, 6, 31) == false; bRet &= isDateValid(2001, 9, 31) == false; bRet &= isDateValid(2001, 11, 31) == false; log.println((bRet ? "[OK]": "[ERR]") + " test_isDateValid"); return bRet; } /** * @return true, wenn year ein Schaltjahr ist */ private static boolean isLeapYear(int year) { return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); } /** * Ueberprueft die Funtkionalitaet von isLeapYear mit ein paar repraesentativen Werten * @return true, wenn alle Tests erfolgreich waren */ private static boolean test_isLeapYear(PrintStream log) { boolean bRet = true; bRet &= isLeapYear(1900) == false; bRet &= isLeapYear(1996) == true; bRet &= isLeapYear(2000) == true; bRet &= isLeapYear(2001) == false; log.println((bRet ? "[OK]": "[ERR]") + " test_isLeapYear"); return bRet; } /** * Berechnet den Wochentag des 1. Januar. * * Wochentag 1.1. = (Jahr + floor((Jahr-1)/4) - floor((Jahr-1)/100) + floor((Jahr-1)/400)) mod 7 * wobei floor(x) für die größte ganze Zahl <= x steht. * * <b>Nicht bekannt ist für welchen Range diese Funktion gültig ist.</b> * * @return 0 für Sonntag, 1 für Montag, ... , 6 für Samstag. */ private static int dayOfWeekOfFirstJanuary(int year) { return (year + (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400) % 7; } /** * Testet <tt>dayOfWeekOfFirstJanuary</tt> */ private static boolean test_dayOfWeekOfFirstJanuary(PrintStream log) { boolean bRet = true; bRet &= dayOfWeekOfFirstJanuary(1999) == 5; bRet &= dayOfWeekOfFirstJanuary(1999) == 5; bRet &= dayOfWeekOfFirstJanuary(2000) == 6; bRet &= dayOfWeekOfFirstJanuary(2001) == 1; log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfWeekOfFirstJanuary"); return bRet; } /** * @throws DateInvalidExcpetion wenn das Datum ungültig ist * @returns Die nummer des Tages im Jahr, 1.1 = 1, 12.31.=365 bze. 366 in einem Schaltjahr */ private static int dayOfYear(int year, int month, int day) throws DateInvalidExcpetion { int ret = 0; if (!isDateValid(year, month, day)) throw new DateInvalidExcpetion(year, month, day); for (int i = 1; i < month; i++) { switch (i) { case 2: ret += 28 + (isLeapYear(year) ? 1 : 0); break; case 4: case 6: case 9: case 11: ret += 30; break; default: ret += 31; break; } } ret += day; return ret; } /** * Testet dayOfYear * @returns true, wenn der Test ok war */ private static boolean test_dayOfYear(PrintStream log) { boolean bRet = true; try { bRet &= dayOfYear(2000, 1, 1) == 1; bRet &= dayOfYear(2000, 3, 1) == 31 + 29 + 1; bRet &= dayOfYear(2000, 12, 31) == 366; bRet &= dayOfYear(2001, 12, 31) == 365; } catch (Exception e) { bRet = false; log.println(e); } log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfYear"); return bRet; } /** * Berechnet den Wochentag zum geg. Datum. * * alternativ zu diesem Verfahren:<br> * - Algorithmus von Christian Zeller: Zeitschrift: Acta Mathematica Nr. 9 (1886/1887)<br> * - Algorithmus von J.D. Robertson: Collected Algorithms (CA) CACM 398 - R1<br> * <br> * eine weiter Moeglichkeit ist die Verwendung der Java Class java.util.GregorianCalendar<br> * * @return liefert den Wochentag zum geg. Datum, 0=So, 1=Mo, ... 6=Sa */ private static int dayOfWeek(int year, int month, int day) throws Exception { if (!isDateValid(year, month, day)) throw new DateInvalidExcpetion(year, month, day); // -1 weil zum 1.1. ja nichts mehr dazu gezählt werden muss return (dayOfWeekOfFirstJanuary(year) + dayOfYear(year, month, day) - 1) % 7; } /** * Testet dayOfWeek * @return true, wenn der Test ok war */ private static boolean test_dayOfWeek(PrintStream log) { boolean bRet = true; try { bRet &= dayOfWeek(1976, 1, 2) == 5; // Fr, ich bin ein "Freitags"kind (c= bRet &= dayOfWeek(2001, 2, 1) == 4; // DO bRet &= dayOfWeek(2001, 12, 24) == 1; // MO } catch (Exception e) { bRet = false; log.println(e); } log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfWeek"); return bRet; } private static String dayOfWeekName(int dayOfWeek) throws Exception { final String[] daysOfWeek = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" }; if (dayOfWeek < 0 || dayOfWeek > 6) throw new Exception("dayOfWeek=" + dayOfWeek + ", muss zwischen 0 und 6 liegen."); return daysOfWeek[dayOfWeek]; } private static String monthNameLong(int month) throws Exception { final String[] months = { "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" }; if (month < 1 || month > 12) throw new Exception("month=" + month + ", muss im Bereich 1 bis 12 liegen."); return months[month - 1]; } private static String monthNameShort(int month) throws Exception { final String[] months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; if (month < 1 || month > 12) throw new Exception("month=" + month + ", muss im Bereich 1 bis 12 liegen."); return months[month - 1]; } /** * Parst das Formatstring und baut den entsprechenden Output zusammen. * * Es wird ein <tt>StringReader</tt> mit 1 Zeichen Lookahead verwendet, * was fuer diese einfache Grammtik vollkommen ausreichend ist. * * @param format eine Folge von<br> * <b>DD</b> Tag des Monats zweistellig (01 - 31)<br> * <b>DAY</b> Wochentagsname (englisch)<br> * <b>MM</b> Monat als Zahl zweistellig (01 - 12)<br> * <b>MON</b> Monat englisch, auf 3 Zeichen abgekürzt<br> * <b>MONTH</b> Monat englisch, ausgeschrieben<br> * <b>YY</b> Jahr zweistellig (die letzten 2 Stellen)<br> * <b>YYYY</b> Jahr vierstellig<br> * * erlaubt Trennzeichen: <b>/ . _ -</b><br> * _ wird auf Space gemappt * * @throws Exception wenn das Formatstring <tt>format</tt> nicht den Regeln * entspricht * @throws DateInvalidException wenn das Datum ungültig ist * @throws Exception wenn eine <tt>IOException</tt> auftritt * * @return Das Datum entsprechend formatiert */ private static String formatDate(int year, int month, int day, String format) throws Exception { StringReader fmt = new StringReader(format); final int EOS = -1; NumberFormat nf2digits = NumberFormat.getNumberInstance(); nf2digits.setMinimumIntegerDigits(2); nf2digits.setGroupingUsed(false); NumberFormat nf4digits = NumberFormat.getNumberInstance(); nf4digits.setMinimumIntegerDigits(4); nf4digits.setGroupingUsed(false); StringBuffer ret = new StringBuffer( 40 ); // 40 sollte fuer die meisten format strings als initial laenge, // gross genug sein if (!isDateValid(year, month, day)) throw new DateInvalidExcpetion(year, month, day); Exception formatException = new Exception("Der Aufbau des Formatstrings \"" + format + "\" ist nicht gueltig"); try { int la = fmt.read(); // lookahead zeichen, vom Typ in, damit -1 auch codiert werden kann, wäre eigentlich // nicht notwendig, da \uFFFF eh nicht im format auftauchen sollte while (la != EOS) { switch ( la ) { case 'D': { la = fmt.read(); if (la == 'D') { // DD ret.append( nf2digits.format(day) ); la = fmt.read(); } else if (la == 'A') { la = fmt.read(); if (la == 'Y') { // DAY ret.append( dayOfWeekName(dayOfWeek(year, month, day)) ); la = fmt.read(); } else throw formatException; } else throw formatException; break; } case 'M': { la = fmt.read(); if (la == 'M') { // MM ret.append( nf2digits.format(month) ); la = fmt.read(); } else if (la == 'O') { // MON | MONTH la = fmt.read(); if (la == 'N') { la = fmt.read(); if (la == 'T') { la = fmt.read(); if (la == 'H') { // MONTH ret.append( monthNameLong(month) ); la = fmt.read(); } else throw formatException; } else { // MON ret.append( monthNameShort(month) ); } } else throw formatException; } else throw formatException; break; } case 'Y': { la = fmt.read(); if (la == 'Y') { // YY | YYYY la = fmt.read(); if (la == 'Y') { la = fmt.read(); if (la == 'Y') { // YYYY ret.append( nf4digits.format(year) ); la = fmt.read(); } else throw formatException; } else { // YY ret.append( nf2digits.format(year % 100) ); } } else throw formatException; break; } case '/': case '.': case '-': // / . - ret.append( (char) la ); la = fmt.read(); break; case '_': // _ ret.append( ' ' ); la = fmt.read(); break; default: throw formatException; // kein break, weil diese zeile eh nicht erreich werden kann } } } catch (Exception e) { throw e; } return ret.toString(); } /** * Hilfsfunktion fuer test_formatDate */ private static boolean test_formatDate_helper(int year, int month, int day, String format, String expectedResult, boolean exceptionExpected) { boolean bRet = true; boolean bException = false; try { String result = formatDate(year, month, day, format); bRet &= result.equals(expectedResult); } catch (Exception e) { bException = true; } bRet &= bException == exceptionExpected; return bRet; } /** * Testet formatDate * @return true, wenn der Test ok war. */ private static boolean test_formatDate(PrintStream log) { boolean bRet = true; bRet &= test_formatDate_helper(1993, 4, 13, "DD.MONTH_YYYY", "13.APRIL 1993", false); bRet &= test_formatDate_helper(1989, 10, 20, "DD_MM_YYYY", "20 10 1989", false); bRet &= test_formatDate_helper(1976, 1, 2, "DD-MM-YYYY", "02-01-1976", false); bRet &= test_formatDate_helper(1976, 1, 2, "DAY_DD/MON.MONTH-MM_YY_YYYY", "FRIDAY 02/JAN.JANUARY-01 76 1976", false); // format string error bRet &= test_formatDate_helper(1976, 1, 2, " ", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "D", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "DDD", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "DA", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "M", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "MO", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "MONT", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "Y", "", true); bRet &= test_formatDate_helper(1976, 1, 2, "YYY", "", true); log.println((bRet ? "[OK]": "[ERR]") + " test_formatDate"); return bRet; } /** * Fuehrt das parsen des Datums und das Formatieren des Datums lt. Spez. durch * @return das Datum formatiert wie in <tt>format</tt> angegeben oder <tt>"FALSCHE EINGABE"</tt> wenn irgendetwas schief gegangen ist */ private static String maskDate(String date, String format) { String ret = ""; try { if (!(checkFormat("##/##/####", date) || checkFormat("##-##-####", date))) throw new Exception("\"" + date + "\" nicht im Format \"##/##/####\" oder \"##-##-####\""); int year = new Integer(date.substring(6, 10)).intValue(); int month = new Integer(date.substring(3, 5)).intValue(); int day = new Integer(date.substring(0, 2)).intValue(); if (!isDateValid(year, month, day)) throw new DateInvalidExcpetion(year, month, day); ret = formatDate(year, month, day, format); } catch (Exception e) { ret = msgFalscheEingabe; } return ret; } /** * Testet die Funktion maskDate * @return true, wenn der Test ok war. */ private static boolean test_maskDate(PrintStream log) { boolean bRet = true; bRet &= maskDate("13-04-1993", "DD.MONTH_YYYY").equals("13.APRIL 1993"); bRet &= maskDate("20/10/1989", "DD_MM_YYYY").equals("20 10 1989"); bRet &= maskDate("29/02.1971", "DD/MON/YY").equals(msgFalscheEingabe); log.println((bRet ? "[OK]": "[ERR]") + " test_maskDate"); return bRet; } } /** * Exception für ein ungültiges Datum. * <br><br> * Yes I know. Das gehöhrt eigentlich in eine eigene Datei aber * da wir nur ein File abgeben dürfen ist es hier. * @see maskdate */ class DateInvalidExcpetion extends Exception { public DateInvalidExcpetion(int year, int month, int day) { super("\"" + day + "-" + month + "-" + year + "\" ist kein gueltiges Datum."); } }