001    /*
002     * Zmanim Java API
003     * Copyright (C) 2004-2010 Eliyahu Hershfeld
004     * 
005     * This program is free software; you can redistribute it and/or modify it under the terms of the
006     * GNU General Public License as published by the Free Software Foundation; either version 2 of the
007     * License, or (at your option) any later version.
008     * 
009     * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
010     * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
011     * General Public License for more details.
012     * 
013     * You should have received a copy of the GNU General Public License along with this program; if
014     * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
015     * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
016     */
017    package net.sourceforge.zmanim.util;
018    
019    import java.lang.reflect.Method;
020    import java.text.DateFormat;
021    import java.text.DecimalFormat;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Date;
025    import java.util.Calendar;
026    import java.util.List;
027    import java.text.SimpleDateFormat;
028    import net.sourceforge.zmanim.*;
029    
030    
031    /**
032     * A class used to format non {@link java.util.Date} times generated by the
033     * Zmanim package. For example the
034     * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour()} returns
035     * the length of the hour in milliseconds. This class can format this time.
036     * 
037     * @author © Eliyahu Hershfeld 2004 - 2010
038     * @version 1.2
039     */
040    public class ZmanimFormatter {
041            private boolean prependZeroHours;
042    
043            private boolean useSeconds;
044    
045            private boolean useMillis;
046    
047            boolean useDecimal;
048    
049            private static DecimalFormat minuteSecondNF = new DecimalFormat("00");;
050    
051            private DecimalFormat hourNF;
052    
053            private static DecimalFormat milliNF = new DecimalFormat("000");
054    
055            private SimpleDateFormat dateFormat;
056    
057            // private DecimalFormat decimalNF;
058    
059            /**
060             * Format using hours, minutes, seconds and milliseconds using the xsd:time
061             * format. This format will return 00.00.00.0 when formatting 0.
062             */
063            public static final int SEXAGESIMAL_XSD_FORMAT = 0;
064    
065            private int timeFormat = SEXAGESIMAL_XSD_FORMAT;
066    
067            /**
068             * Format using standard decimal format with 5 positions after the decimal.
069             */
070            public static final int DECIMAL_FORMAT = 1;
071    
072            /** Format using hours and minutes. */
073            public static final int SEXAGESIMAL_FORMAT = 2;
074    
075            /** Format using hours, minutes and seconds. */
076            public static final int SEXAGESIMAL_SECONDS_FORMAT = 3;
077    
078            /** Format using hours, minutes, seconds and milliseconds. */
079            public static final int SEXAGESIMAL_MILLIS_FORMAT = 4;
080            
081            /** constant for milliseconds in a minute (60,000) */
082            static final long MINUTE_MILLIS = 60 * 1000;
083            
084            /** constant for milliseconds in an hour (3,600,000) */
085            public static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
086    
087            /**
088             * Format using the XSD Duration format. This is in the format of
089             * PT1H6M7.869S (P for period (duration), T for time, H, M and S indicate
090             * hours, minutes and seconds.
091             */
092            public static final int XSD_DURATION_FORMAT = 5;
093    
094            public ZmanimFormatter() {
095                    this(0, new SimpleDateFormat("h:mm:ss"));
096            }
097    
098            /**
099             * ZmanimFormatter constructor using a formatter
100             * 
101             * @param format
102             *            int The formatting style to use. Using
103             *            ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the
104             *            time time of 90*60*1000 + 1 as 1:30:00
105             */
106            public ZmanimFormatter(int format, SimpleDateFormat dateFormat) {
107                    String hourFormat = "0";
108                    if (prependZeroHours) {
109                            hourFormat = "00";
110                    }
111                    hourNF = new DecimalFormat(hourFormat);
112                    // decimalNF = new DecimalFormat("0.0####");
113                    setTimeFormat(format);
114                    this.setDateFormat(dateFormat);
115            }
116    
117            /**
118             * Sets the format to use for formatting.
119             * 
120             * @param format
121             *            int the format constant to use.
122             */
123            public void setTimeFormat(int format) {
124                    timeFormat = format;
125                    switch (format) {
126                    case SEXAGESIMAL_XSD_FORMAT:
127                            setSettings(true, true, true);
128                            break;
129                    case SEXAGESIMAL_FORMAT:
130                            setSettings(false, false, false);
131                            break;
132                    case SEXAGESIMAL_SECONDS_FORMAT:
133                            setSettings(false, true, false);
134                            break;
135                    case SEXAGESIMAL_MILLIS_FORMAT:
136                            setSettings(false, true, true);
137                            break;
138                    case DECIMAL_FORMAT:
139                    default:
140                            useDecimal = true;
141                    }
142            }
143    
144            public void setDateFormat(SimpleDateFormat sdf) {
145                    dateFormat = sdf;
146            }
147    
148            public SimpleDateFormat getDateFormat() {
149                    return dateFormat;
150            }
151    
152            private void setSettings(boolean prependZeroHours, boolean useSeconds,
153                            boolean useMillis) {
154                    this.prependZeroHours = prependZeroHours;
155                    this.useSeconds = useSeconds;
156                    this.useMillis = useMillis;
157            }
158    
159            /**
160             * A method that formats milliseconds into a time format.
161             * 
162             * @param milliseconds
163             *            The time in milliseconds.
164             * @return String The formatted <code>String</code>
165             */
166            public String format(double milliseconds) {
167                    return format((int) milliseconds);
168            }
169    
170            /**
171             * A method that formats milliseconds into a time format.
172             * 
173             * @param millis
174             *            The time in milliseconds.
175             * @return String The formatted <code>String</code>
176             */
177            public String format(int millis) {
178                    return format(new Time(millis));
179            }
180    
181            /**
182             * A method that formats {@link Time}objects.
183             * 
184             * @param time
185             *            The time <code>Object</code> to be formatted.
186             * @return String The formatted <code>String</code>
187             */
188            public String format(Time time) {
189                    if (timeFormat == XSD_DURATION_FORMAT) {
190                            return formatXSDDurationTime(time);
191                    }
192                    StringBuffer sb = new StringBuffer();
193                    sb.append(hourNF.format(time.getHours()));
194                    sb.append(":");
195                    sb.append(minuteSecondNF.format(time.getMinutes()));
196                    if (useSeconds) {
197                            sb.append(":");
198                            sb.append(minuteSecondNF.format(time.getSeconds()));
199                    }
200                    if (useMillis) {
201                            sb.append(".");
202                            sb.append(milliNF.format(time.getMilliseconds()));
203                    }
204                    return sb.toString();
205            }
206    
207            /**
208             * Formats a date using this classe's {@link #getDateFormat() date format}.
209             * 
210             * @param dateTime
211             *            the date to format
212             * @param calendar
213             *            the {@link java.util.Calendar Calendar} used to help format
214             *            based on the Calendar's DST and other settings.
215             * @return the formatted String
216             */
217            public String formatDateTime(Date dateTime, Calendar calendar) {
218                    dateFormat.setCalendar(calendar);
219                    if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) {
220                            return getXSDateTime(dateTime, calendar);
221                    } else {
222                            return dateFormat.format(dateTime);
223                    }
224    
225            }
226    
227            /**
228             * The date:date-time function returns the current date and time as a
229             * date/time string. The date/time string that's returned must be a string
230             * in the format defined as the lexical representation of xs:dateTime in <a
231             * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a>
232             * of <a href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
233             * Datatypes]</a>. The date/time format is basically CCYY-MM-DDThh:mm:ss,
234             * although implementers should consult <a
235             * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
236             * Datatypes]</a> and <a href="http://www.iso.ch/markete/8601.pdf">[ISO
237             * 8601]</a> for details. The date/time string format must include a time
238             * zone, either a Z to indicate Coordinated Universal Time or a + or -
239             * followed by the difference between the difference from UTC represented as
240             * hh:mm.
241             */
242            public String getXSDateTime(Date dateTime, Calendar cal) {
243                    String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
244                    /*
245                     * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) {
246                     * xmlDateFormat = xsdDateTimeFormat; }
247                     */
248                    SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat);
249    
250                    StringBuffer buff = new StringBuffer(dateFormat.format(dateTime));
251                    // Must also include offset from UTF.
252                    // Get the offset (in milliseconds).
253                    int offset = cal.get(Calendar.ZONE_OFFSET)
254                                    + cal.get(Calendar.DST_OFFSET);
255                    // If there is no offset, we have "Coordinated
256                    // Universal Time."
257                    if (offset == 0)
258                            buff.append("Z");
259                    else {
260                            // Convert milliseconds to hours and minutes
261                            int hrs = offset / (60 * 60 * 1000);
262                            // In a few cases, the time zone may be +/-hh:30.
263                            int min = offset % (60 * 60 * 1000);
264                            char posneg = hrs < 0 ? '-' : '+';
265                            buff.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
266                    }
267                    return buff.toString();
268            }
269    
270            /**
271             * Represent the hours and minutes with two-digit strings.
272             * 
273             * @param digits hours or minutes.
274             * @return two-digit String representation of hrs or minutes.
275             */
276            private static String formatDigits(int digits) {
277                    String dd = String.valueOf(Math.abs(digits));
278                    return dd.length() == 1 ? '0' + dd : dd;
279            }
280    
281            /**
282             * This returns the xml representation of an xsd:duration object.
283             * 
284             * @param millis the duration in milliseconds
285             * @return the xsd:duration formatted String
286             */
287            public String formatXSDDurationTime(long millis) {
288                    return formatXSDDurationTime(new Time(millis));
289            }
290    
291            /**
292             * This returns the xml representation of an xsd:duration object.
293             * 
294             * @param time the duration as a Time object 
295             * @return the xsd:duration formatted String
296             */
297            public String formatXSDDurationTime(Time time) {
298                    StringBuffer duration = new StringBuffer();
299    
300                    duration.append("P");
301    
302                    if (time.getHours() != 0 || time.getMinutes() != 0
303                                    || time.getSeconds() != 0 || time.getMilliseconds() != 0) {
304                            duration.append("T");
305    
306                            if (time.getHours() != 0)
307                                    duration.append(time.getHours() + "H");
308    
309                            if (time.getMinutes() != 0)
310                                    duration.append(time.getMinutes() + "M");
311    
312                            if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
313                                    duration.append(time.getSeconds() + "."
314                                                    + milliNF.format(time.getMilliseconds()));
315                                    duration.append("S");
316                            }
317                            if (duration.length() == 1) // zero seconds
318                                    duration.append("T0S");
319                            if (time.isNegative())
320                                    duration.insert(0, "-");
321                    }
322                    return duration.toString();
323            }
324            
325            /**
326             * A method that returns an XML formatted <code>String</code> representing
327             * the serialized <code>Object</code>. The format used is:
328             * 
329             * <pre>
330             *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
331             *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
332             *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
333             *     ...
334             *   &lt;/AstronomicalTimes&gt;
335             * </pre>
336             * 
337             * Note that the output uses the <a
338             * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a>
339             * format for times such as sunrise, and <a
340             * href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a>
341             * format for times that are a duration such as the length of a
342             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
343             * returned by the {@link #toString() toString} }.
344             * 
345             * @return The XML formatted <code>String</code>. The format will be:
346             * 
347             * <pre>
348             *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
349             *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
350             *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
351             *     ...
352             *  &lt;/AstronomicalTimes&gt;
353             * </pre>
354             * 
355             */
356            public static String toXML(AstronomicalCalendar ac) {
357                    ZmanimFormatter formatter = new ZmanimFormatter(
358                                    ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
359                                                    "yyyy-MM-dd'T'HH:mm:ss"));
360                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
361                    String output = "<";
362                    if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
363                            output += "AstronomicalTimes";
364                    } else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
365                            output += "Zmanim";
366                    }
367                    output += " date=\"" + df.format(ac.getCalendar().getTime()) + "\"";
368                    output += " type=\"" + ac.getClass().getName() + "\"";
369                    output += " algorithm=\""
370                                    + ac.getAstronomicalCalculator().getCalculatorName() + "\"";
371                    output += " location=\"" + ac.getGeoLocation().getLocationName() + "\"";
372                    output += " latitude=\"" + ac.getGeoLocation().getLatitude() + "\"";
373                    output += " longitude=\"" + ac.getGeoLocation().getLongitude() + "\"";
374                    output += " elevation=\"" + ac.getGeoLocation().getElevation() + "\"";
375                    output += " timeZoneName=\""
376                                    + ac.getGeoLocation().getTimeZone().getDisplayName() + "\"";
377                    output += " timeZoneID=\"" + ac.getGeoLocation().getTimeZone().getID()
378                                    + "\"";
379                    output += " timeZoneOffset=\""
380                                    + (ac.getGeoLocation().getTimeZone().getOffset(
381                                                    ac.getCalendar().getTimeInMillis()) / ((double)HOUR_MILLIS))
382                                    + "\"";
383    
384                    output += ">\n";
385    
386                    Method[] theMethods = ac.getClass().getMethods();
387                    String tagName = "";
388                    Object value = null;
389                    List dateList = new ArrayList();
390                    List durationList = new ArrayList();
391                    List otherList = new ArrayList();
392                    for (int i = 0; i < theMethods.length; i++) {
393                            if (includeMethod(theMethods[i])) {
394                                    tagName = theMethods[i].getName().substring(3);
395                                    //String returnType = theMethods[i].getReturnType().getName();
396                                    try {
397                                            value = theMethods[i].invoke(ac, (Object[]) null);
398                                            if (value == null) {//FIXME: use reflection to determine what the return type is, not the value
399                                                    otherList.add("<" + tagName + ">N/A</" + tagName + ">");
400                                            } else if (value instanceof Date) {
401                                                    dateList.add(new Zman((Date) value, tagName));
402                                            } else if (value instanceof Long) {// shaah zmanis
403                                                    durationList.add(new Zman((int) ((Long) value)
404                                                                    .longValue(), tagName));
405                                            } else { // will probably never enter this block, but is
406                                                    // present to be future proof
407                                                    otherList.add("<" + tagName + ">" + value + "</"
408                                                                    + tagName + ">");
409                                            }
410                                    } catch (Exception e) {
411                                            e.printStackTrace();
412                                    }
413                            }
414                    }
415                    Zman zman;
416                    Collections.sort(dateList, Zman.DATE_ORDER);
417                    for (int i = 0; i < dateList.size(); i++) {
418                            zman = (Zman) dateList.get(i);
419                            output += "\t<" + zman.getZmanLabel();
420                            output += ">";
421                            output += formatter.formatDateTime(zman.getZman(), ac.getCalendar())
422                                            + "</" + zman.getZmanLabel() + ">\n";
423                    }
424                    Collections.sort(durationList, Zman.DURATION_ORDER);
425                    for (int i = 0; i < durationList.size(); i++) {
426                            zman = (Zman) durationList.get(i);
427                            output += "\t<" + zman.getZmanLabel();
428                            output += ">";
429                            output += formatter.format((int) zman.getDuration()) + "</"
430                                            + zman.getZmanLabel() + ">\n";
431                    }
432    
433                    for (int i = 0; i < otherList.size(); i++) {// will probably never enter
434                            // this block
435                            output += "\t" + otherList.get(i) + "\n";
436                    }
437    
438                    if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
439                            output += "</AstronomicalTimes>";
440                    } else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
441                            output += "</Zmanim>";
442                    }
443                    return output;
444            }
445            
446            /**
447             * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
448             * 
449             * @param method
450             * @return
451             */
452            private static boolean includeMethod(Method method) {
453                    List methodWhiteList = new ArrayList();
454                    // methodWhiteList.add("getName");
455    
456                    List methodBlackList = new ArrayList();
457                    // methodBlackList.add("getGregorianChange");
458    
459                    if (methodWhiteList.contains(method.getName()))
460                            return true;
461                    if (methodBlackList.contains(method.getName()))
462                            return false;
463    
464                    if (method.getParameterTypes().length > 0)
465                            return false; // Skip get methods with parameters since we do not
466                    // know what value to pass
467                    if (!method.getName().startsWith("get"))
468                            return false;
469    
470                    if (method.getReturnType().getName().endsWith("Date")
471                                    || method.getReturnType().getName().endsWith("long")) {
472                            return true;
473                    }
474                    return false;
475            }
476    }