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