001/*
002 * Zmanim Java API
003 * Copyright (C) 2004-2022 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: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
015 */
016package com.kosherjava.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 com.kosherjava.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 com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour()} returns the length of the hour in
033 * milliseconds. This class can format this time.
034 * 
035 * @author © Eliyahu Hershfeld 2004 - 2022
036 */
037public class ZmanimFormatter {
038        /**
039         * Setting to prepent a zero to single digit hours.
040         * @see #setSettings(boolean, boolean, boolean)
041         */
042        private boolean prependZeroHours = false;
043
044        /**
045         * @see #setSettings(boolean, boolean, boolean)
046         */
047        private boolean useSeconds = false;
048
049        /**
050         * @see #setSettings(boolean, boolean, boolean)
051         */
052        private boolean useMillis = false;
053
054        /**
055         * the formatter for minutes as seconds.
056         */
057        private static DecimalFormat minuteSecondNF = new DecimalFormat("00");
058
059        /**
060         * the formatter for hours.
061         */
062        private DecimalFormat hourNF;
063
064        /**
065         * the formatter for minutes as milliseconds.
066         */
067        private static DecimalFormat milliNF = new DecimalFormat("000");
068
069        /**
070         * @see #setDateFormat(SimpleDateFormat)
071         */
072        private SimpleDateFormat dateFormat;
073
074        /**
075         * @see #setTimeZone(TimeZone)
076         */
077        private TimeZone timeZone = null; // TimeZone.getTimeZone("UTC");
078
079        // private DecimalFormat decimalNF;
080
081        /**
082         * @return the timeZone
083         */
084        public TimeZone getTimeZone() {
085                return timeZone;
086        }
087
088        /**
089         * @param timeZone
090         *            the timeZone to set
091         */
092        public void setTimeZone(TimeZone timeZone) {
093                this.timeZone = timeZone;
094        }
095
096        /**
097         * Format using hours, minutes, seconds and milliseconds using the xsd:time format. This format will return
098         * 00.00.00.0 when formatting 0.
099         */
100        public static final int SEXAGESIMAL_XSD_FORMAT = 0;
101
102        /**
103         * Defaults to {@link #SEXAGESIMAL_XSD_FORMAT}.
104         * @see #setTimeFormat(int)
105         */
106        private int timeFormat = SEXAGESIMAL_XSD_FORMAT;
107
108        /**
109         * Format using standard decimal format with 5 positions after the decimal.
110         */
111        public static final int DECIMAL_FORMAT = 1;
112
113        /** Format using hours and minutes. */
114        public static final int SEXAGESIMAL_FORMAT = 2;
115
116        /** Format using hours, minutes and seconds. */
117        public static final int SEXAGESIMAL_SECONDS_FORMAT = 3;
118
119        /** Format using hours, minutes, seconds and milliseconds. */
120        public static final int SEXAGESIMAL_MILLIS_FORMAT = 4;
121
122        /** constant for milliseconds in a minute (60,000) */
123        static final long MINUTE_MILLIS = 60 * 1000;
124
125        /** constant for milliseconds in an hour (3,600,000) */
126        public static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
127
128        /**
129         * Format using the XSD Duration format. This is in the format of PT1H6M7.869S (P for period (duration), T for time,
130         * H, M and S indicate hours, minutes and seconds.
131         */
132        public static final int XSD_DURATION_FORMAT = 5;
133
134        /**
135         * constructor that defaults to this will use the format "h:mm:ss" for dates and 00.00.00.0 for {@link Time}.
136         * @param timeZone the TimeZone Object
137         */
138        public ZmanimFormatter(TimeZone timeZone) {
139                this(0, new SimpleDateFormat("h:mm:ss"), timeZone);
140        }
141
142        // public ZmanimFormatter() {
143        // this(0, new SimpleDateFormat("h:mm:ss"), TimeZone.getTimeZone("UTC"));
144        // }
145
146        /**
147         * ZmanimFormatter constructor using a formatter
148         * 
149         * @param format
150         *            int The formatting style to use. Using ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the time
151         *            time of 90*60*1000 + 1 as 1:30:00
152         * @param dateFormat the SimpleDateFormat Object
153         * @param timeZone the TimeZone Object
154         */
155        public ZmanimFormatter(int format, SimpleDateFormat dateFormat, TimeZone timeZone) {
156                setTimeZone(timeZone);
157                String hourFormat = "0";
158                if (prependZeroHours) {
159                        hourFormat = "00";
160                }
161                this.hourNF = new DecimalFormat(hourFormat);
162                setTimeFormat(format);
163                dateFormat.setTimeZone(timeZone);
164                setDateFormat(dateFormat);
165        }
166
167        /**
168         * Sets the format to use for formatting.
169         * 
170         * @param format
171         *            int the format constant to use.
172         */
173        public void setTimeFormat(int format) {
174                this.timeFormat = format;
175                switch (format) {
176                case SEXAGESIMAL_XSD_FORMAT:
177                        setSettings(true, true, true);
178                        break;
179                case SEXAGESIMAL_FORMAT:
180                        setSettings(false, false, false);
181                        break;
182                case SEXAGESIMAL_SECONDS_FORMAT:
183                        setSettings(false, true, false);
184                        break;
185                case SEXAGESIMAL_MILLIS_FORMAT:
186                        setSettings(false, true, true);
187                        break;
188                // case DECIMAL_FORMAT:
189                // default:
190                }
191        }
192
193        /**
194         * Sets the SimpleDateFormat Object
195         * @param simpleDateFormat the SimpleDateFormat Object to set
196         */
197        public void setDateFormat(SimpleDateFormat simpleDateFormat) {
198                this.dateFormat = simpleDateFormat;
199        }
200
201        /**
202         * returns the SimpleDateFormat Object
203         * @return the SimpleDateFormat Object
204         */
205        public SimpleDateFormat getDateFormat() {
206                return this.dateFormat;
207        }
208
209        /**
210         * Sets various format settings.
211         * @param prependZeroHours  if to prepend a zero for single digit hours (so that 1 'oclock is displayed as 01)
212         * @param useSeconds should seconds be used in the time format
213         * @param useMillis should milliseconds be used informatting time.
214         */
215        private void setSettings(boolean prependZeroHours, boolean useSeconds, boolean useMillis) {
216                this.prependZeroHours = prependZeroHours;
217                this.useSeconds = useSeconds;
218                this.useMillis = useMillis;
219        }
220
221        /**
222         * A method that formats milliseconds into a time format.
223         * 
224         * @param milliseconds
225         *            The time in milliseconds.
226         * @return String The formatted <code>String</code>
227         */
228        public String format(double milliseconds) {
229                return format((int) milliseconds);
230        }
231
232        /**
233         * A method that formats milliseconds into a time format.
234         * 
235         * @param millis
236         *            The time in milliseconds.
237         * @return String The formatted <code>String</code>
238         */
239        public String format(int millis) {
240                return format(new Time(millis));
241        }
242
243        /**
244         * A method that formats {@link Time}objects.
245         * 
246         * @param time
247         *            The time <code>Object</code> to be formatted.
248         * @return String The formatted <code>String</code>
249         */
250        public String format(Time time) {
251                if (this.timeFormat == XSD_DURATION_FORMAT) {
252                        return formatXSDDurationTime(time);
253                }
254                StringBuilder sb = new StringBuilder();
255                sb.append(this.hourNF.format(time.getHours()));
256                sb.append(":");
257                sb.append(minuteSecondNF.format(time.getMinutes()));
258                if (this.useSeconds) {
259                        sb.append(":");
260                        sb.append(minuteSecondNF.format(time.getSeconds()));
261                }
262                if (this.useMillis) {
263                        sb.append(".");
264                        sb.append(milliNF.format(time.getMilliseconds()));
265                }
266                return sb.toString();
267        }
268
269        /**
270         * Formats a date using this classe's {@link #getDateFormat() date format}.
271         * 
272         * @param dateTime
273         *            the date to format
274         * @param calendar
275         *            the {@link java.util.Calendar Calendar} used to help format based on the Calendar's DST and other
276         *            settings.
277         * @return the formatted String
278         */
279        public String formatDateTime(Date dateTime, Calendar calendar) {
280                this.dateFormat.setCalendar(calendar);
281                if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) {
282                        return getXSDateTime(dateTime, calendar);
283                } else {
284                        return this.dateFormat.format(dateTime);
285                }
286
287        }
288
289        /**
290         * The date:date-time function returns the current date and time as a date/time string. The date/time string that's
291         * returned must be a string in the format defined as the lexical representation of xs:dateTime in <a
292         * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a> of <a
293         * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2: Datatypes]</a>. The date/time format is
294         * basically CCYY-MM-DDThh:mm:ss, although implementers should consult <a
295         * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2: Datatypes]</a> and <a
296         * href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details. The date/time string format must include a
297         * time zone, either a Z to indicate Coordinated Universal Time or a + or - followed by the difference between the
298         * difference from UTC represented as hh:mm.
299         * @param dateTime the Date Object
300         * @param calendar Calendar Object
301         * @return the XSD dateTime
302         */
303        public String getXSDateTime(Date dateTime, Calendar calendar) {
304                String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
305                /*
306                 * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) { xmlDateFormat = xsdDateTimeFormat; }
307                 */
308                SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat);
309                dateFormat.setTimeZone(getTimeZone());
310
311                StringBuilder sb = new StringBuilder(dateFormat.format(dateTime));
312                // Must also include offset from UTF.
313                int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);// Get the offset (in milliseconds)
314                // If there is no offset, we have "Coordinated Universal Time"
315                if (offset == 0)
316                        sb.append("Z");
317                else {
318                        // Convert milliseconds to hours and minutes
319                        int hrs = offset / (60 * 60 * 1000);
320                        // In a few cases, the time zone may be +/-hh:30.
321                        int min = offset % (60 * 60 * 1000);
322                        char posneg = hrs < 0 ? '-' : '+';
323                        sb.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
324                }
325                return sb.toString();
326        }
327
328        /**
329         * Represent the hours and minutes with two-digit strings.
330         * 
331         * @param digits
332         *            hours or minutes.
333         * @return two-digit String representation of hrs or minutes.
334         */
335        private static String formatDigits(int digits) {
336                String dd = String.valueOf(Math.abs(digits));
337                return dd.length() == 1 ? '0' + dd : dd;
338        }
339
340        /**
341         * This returns the xml representation of an xsd:duration object.
342         * 
343         * @param millis
344         *            the duration in milliseconds
345         * @return the xsd:duration formatted String
346         */
347        public String formatXSDDurationTime(long millis) {
348                return formatXSDDurationTime(new Time(millis));
349        }
350
351        /**
352         * This returns the xml representation of an xsd:duration object.
353         * 
354         * @param time
355         *            the duration as a Time object
356         * @return the xsd:duration formatted String
357         */
358        public String formatXSDDurationTime(Time time) {
359                StringBuilder duration = new StringBuilder();
360                if (time.getHours() != 0 || time.getMinutes() != 0 || time.getSeconds() != 0 || time.getMilliseconds() != 0) {
361                        duration.append("P");
362                        duration.append("T");
363
364                        if (time.getHours() != 0)
365                                duration.append(time.getHours() + "H");
366
367                        if (time.getMinutes() != 0)
368                                duration.append(time.getMinutes() + "M");
369
370                        if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
371                                duration.append(time.getSeconds() + "." + milliNF.format(time.getMilliseconds()));
372                                duration.append("S");
373                        }
374                        if (duration.length() == 1) // zero seconds
375                                duration.append("T0S");
376                        if (time.isNegative())
377                                duration.insert(0, "-");
378                }
379                return duration.toString();
380        }
381
382        /**
383         * A method that returns an XML formatted <code>String</code> representing the serialized <code>Object</code>. The
384         * format used is:
385         * 
386         * <pre>
387         *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;com.kosherjava.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;
388         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
389         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
390         *     ...
391         *   &lt;/AstronomicalTimes&gt;
392         * </pre>
393         * 
394         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
395         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
396         * times that are a duration such as the length of a
397         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
398         * returned by the {@link #toString() toString}.
399         * 
400         * @param astronomicalCalendar the AstronomicalCalendar Object
401         * 
402         * @return The XML formatted <code>String</code>. The format will be:
403         * 
404         *         <pre>
405         *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;com.kosherjava.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;
406         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
407         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
408         *     ...
409         *  &lt;/AstronomicalTimes&gt;
410         * </pre>
411         * 
412         * @todo Add proper schema, and support for nulls. XSD duration (for solar hours), should probably return nil and not P.
413         */
414        public static String toXML(AstronomicalCalendar astronomicalCalendar) {
415                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
416                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getTimeZone());
417                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
418                df.setTimeZone(astronomicalCalendar.getGeoLocation().getTimeZone());
419
420                Date date = astronomicalCalendar.getCalendar().getTime();
421                TimeZone tz = astronomicalCalendar.getGeoLocation().getTimeZone();
422                boolean daylight = tz.useDaylightTime() && tz.inDaylightTime(date);
423
424                StringBuilder sb = new StringBuilder("<");
425                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
426                        sb.append("AstronomicalTimes");
427                        // TODO: use proper schema ref, and maybe build a real schema.
428                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
429                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim astronomical.xsd"
430                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
431                        sb.append("Zmanim");
432                        // TODO: use proper schema ref, and maybe build a real schema.
433                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
434                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim zmanim.xsd"
435                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
436                        sb.append("BasicZmanim");
437                        // TODO: use proper schema ref, and maybe build a real schema.
438                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
439                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim basicZmanim.xsd"
440                }
441                sb.append(" date=\"").append(df.format(date)).append("\"");
442                sb.append(" type=\"").append(astronomicalCalendar.getClass().getName()).append("\"");
443                sb.append(" algorithm=\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\"");
444                sb.append(" location=\"").append(astronomicalCalendar.getGeoLocation().getLocationName()).append("\"");
445                sb.append(" latitude=\"").append(astronomicalCalendar.getGeoLocation().getLatitude()).append("\"");
446                sb.append(" longitude=\"").append(astronomicalCalendar.getGeoLocation().getLongitude()).append("\"");
447                sb.append(" elevation=\"").append(astronomicalCalendar.getGeoLocation().getElevation()).append("\"");
448                sb.append(" timeZoneName=\"").append(tz.getDisplayName(daylight, TimeZone.LONG)).append("\"");
449                sb.append(" timeZoneID=\"").append(tz.getID()).append("\"");
450                sb.append(" timeZoneOffset=\"")
451                                .append((tz.getOffset(astronomicalCalendar.getCalendar().getTimeInMillis()) / ((double) HOUR_MILLIS)))
452                                .append("\"");
453
454                sb.append(">\n");
455
456                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
457                String tagName = "";
458                Object value = null;
459                List<Zman> dateList = new ArrayList<Zman>();
460                List<Zman> durationList = new ArrayList<Zman>();
461                List<String> otherList = new ArrayList<String>();
462                for (int i = 0; i < theMethods.length; i++) {
463                        if (includeMethod(theMethods[i])) {
464                                tagName = theMethods[i].getName().substring(3);
465                                // String returnType = theMethods[i].getReturnType().getName();
466                                try {
467                                        value = theMethods[i].invoke(astronomicalCalendar, (Object[]) null);
468                                        if (value == null) {// TODO: Consider using reflection to determine the return type, not the value
469                                                otherList.add("<" + tagName + ">N/A</" + tagName + ">");
470                                                // TODO: instead of N/A, consider return proper xs:nil.
471                                                // otherList.add("<" + tagName + " xs:nil=\"true\" />");
472                                        } else if (value instanceof Date) {
473                                                dateList.add(new Zman((Date) value, tagName));
474                                        } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis
475                                                if (((Long) value).longValue() == Long.MIN_VALUE) {
476                                                        otherList.add("<" + tagName + ">N/A</" + tagName + ">");
477                                                        // TODO: instead of N/A, consider return proper xs:nil.
478                                                        // otherList.add("<" + tagName + " xs:nil=\"true\" />");
479                                                } else {
480                                                        durationList.add(new Zman((int) ((Long) value).longValue(), tagName));
481                                                }
482                                        } else { // will probably never enter this block, but is present to be future proof
483                                                otherList.add("<" + tagName + ">" + value + "</" + tagName + ">");
484                                        }
485                                } catch (Exception e) {
486                                        e.printStackTrace();
487                                }
488                        }
489                }
490                Zman zman;
491                Collections.sort(dateList, Zman.DATE_ORDER);
492
493                for (int i = 0; i < dateList.size(); i++) {
494                        zman = (Zman) dateList.get(i);
495                        sb.append("\t<").append(zman.getLabel()).append(">");
496                        sb.append(formatter.formatDateTime(zman.getZman(), astronomicalCalendar.getCalendar()));
497                        sb.append("</").append(zman.getLabel()).append(">\n");
498                }
499                Collections.sort(durationList, Zman.DURATION_ORDER);
500                for (int i = 0; i < durationList.size(); i++) {
501                        zman = (Zman) durationList.get(i);
502                        sb.append("\t<" + zman.getLabel()).append(">");
503                        sb.append(formatter.format((int) zman.getDuration())).append("</").append(zman.getLabel())
504                                        .append(">\n");
505                }
506
507                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
508                        sb.append("\t").append(otherList.get(i)).append("\n");
509                }
510
511                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
512                        sb.append("</AstronomicalTimes>");
513                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
514                        sb.append("</Zmanim>");
515                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
516                        sb.append("</BasicZmanim>");
517                }
518                return sb.toString();
519        }
520        
521        /**
522         * A method that returns a JSON formatted <code>String</code> representing the serialized <code>Object</code>. The
523         * format used is:
524         * <pre>
525         * {
526         *    &quot;metadata&quot;:{
527         *      &quot;date&quot;:&quot;1969-02-08&quot;,
528         *      &quot;type&quot;:&quot;com.kosherjava.zmanim.AstronomicalCalendar&quot;,
529         *      &quot;algorithm&quot;:&quot;US Naval Almanac Algorithm&quot;,
530         *      &quot;location&quot;:&quot;Lakewood, NJ&quot;,
531         *      &quot;latitude&quot;:&quot;40.095965&quot;,
532         *      &quot;longitude&quot;:&quot;-74.22213&quot;,
533         *      &quot;elevation:&quot;31.0&quot;,
534         *      &quot;timeZoneName&quot;:&quot;Eastern Standard Time&quot;,
535         *      &quot;timeZoneID&quot;:&quot;America/New_York&quot;,
536         *      &quot;timeZoneOffset&quot;:&quot;-5&quot;},
537         *    &quot;AstronomicalTimes&quot;:{
538         *     &quot;Sunrise&quot;:&quot;2007-02-18T06:45:27-05:00&quot;,
539         *     &quot;TemporalHour&quot;:&quot;PT54M17.529S&quot;
540         *     ...
541         *     }
542         * }
543         * </pre>
544         * 
545         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
546         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
547         * times that are a duration such as the length of a
548         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}.
549         * 
550         * @param astronomicalCalendar the AstronomicalCalendar Object
551         * 
552         * @return The JSON formatted <code>String</code>. The format will be:
553         * <pre>
554         * {
555         *    &quot;metadata&quot;:{
556         *      &quot;date&quot;:&quot;1969-02-08&quot;,
557         *      &quot;type&quot;:&quot;com.kosherjava.zmanim.AstronomicalCalendar&quot;,
558         *      &quot;algorithm&quot;:&quot;US Naval Almanac Algorithm&quot;,
559         *      &quot;location&quot;:&quot;Lakewood, NJ&quot;,
560         *      &quot;latitude&quot;:&quot;40.095965&quot;,
561         *      &quot;longitude&quot;:&quot;-74.22213&quot;,
562         *      &quot;elevation:&quot;31.0&quot;,
563         *      &quot;timeZoneName&quot;:&quot;Eastern Standard Time&quot;,
564         *      &quot;timeZoneID&quot;:&quot;America/New_York&quot;,
565         *      &quot;timeZoneOffset&quot;:&quot;-5&quot;},
566         *    &quot;AstronomicalTimes&quot;:{
567         *     &quot;Sunrise&quot;:&quot;2007-02-18T06:45:27-05:00&quot;,
568         *     &quot;TemporalHour&quot;:&quot;PT54M17.529S&quot;
569         *     ...
570         *     }
571         * }
572         * </pre>
573         */
574        public static String toJSON(AstronomicalCalendar astronomicalCalendar) {
575                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
576                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getTimeZone());
577                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
578                df.setTimeZone(astronomicalCalendar.getGeoLocation().getTimeZone());
579
580                Date date = astronomicalCalendar.getCalendar().getTime();
581                TimeZone tz = astronomicalCalendar.getGeoLocation().getTimeZone();
582                boolean daylight = tz.useDaylightTime() && tz.inDaylightTime(date);
583
584                StringBuilder sb = new StringBuilder("{\n\"metadata\":{\n");
585                sb.append("\t\"date\":\"").append(df.format(date)).append("\",\n");
586                sb.append("\t\"type\":\"").append(astronomicalCalendar.getClass().getName()).append("\",\n");
587                sb.append("\t\"algorithm\":\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\",\n");
588                sb.append("\t\"location\":\"").append(astronomicalCalendar.getGeoLocation().getLocationName()).append("\",\n");
589                sb.append("\t\"latitude\":\"").append(astronomicalCalendar.getGeoLocation().getLatitude()).append("\",\n");
590                sb.append("\t\"longitude\":\"").append(astronomicalCalendar.getGeoLocation().getLongitude()).append("\",\n");
591                sb.append("\t\"elevation\":\"").append(astronomicalCalendar.getGeoLocation().getElevation()).append("\",\n");
592                sb.append("\t\"timeZoneName\":\"").append(tz.getDisplayName(daylight, TimeZone.LONG)).append("\",\n");
593                sb.append("\t\"timeZoneID\":\"").append(tz.getID()).append("\",\n");
594                sb.append("\t\"timeZoneOffset\":\"")
595                                .append((tz.getOffset(astronomicalCalendar.getCalendar().getTimeInMillis()) / ((double) HOUR_MILLIS)))
596                                .append("\"");
597
598                sb.append("},\n\"");
599                
600                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
601                        sb.append("AstronomicalTimes");
602                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
603                        sb.append("Zmanim");
604                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
605                        sb.append("BasicZmanim");
606                }
607                sb.append("\":{\n");
608                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
609                String tagName = "";
610                Object value = null;
611                List<Zman> dateList = new ArrayList<Zman>();
612                List<Zman> durationList = new ArrayList<Zman>();
613                List<String> otherList = new ArrayList<String>();
614                for (int i = 0; i < theMethods.length; i++) {
615                        if (includeMethod(theMethods[i])) {
616                                tagName = theMethods[i].getName().substring(3);
617                                // String returnType = theMethods[i].getReturnType().getName();
618                                try {
619                                        value = theMethods[i].invoke(astronomicalCalendar, (Object[]) null);
620                                        if (value == null) {// TODO: Consider using reflection to determine the return type, not the value
621                                                otherList.add("\"" + tagName + "\":\"N/A\",");
622                                        } else if (value instanceof Date) {
623                                                dateList.add(new Zman((Date) value, tagName));
624                                        } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis
625                                                if (((Long) value).longValue() == Long.MIN_VALUE) {
626                                                        otherList.add("\"" + tagName + "\":\"N/A\"");
627                                                } else {
628                                                        durationList.add(new Zman((int) ((Long) value).longValue(), tagName));
629                                                }
630                                        } else { // will probably never enter this block, but is present to be future proof
631                                                otherList.add("\"" + tagName + "\":\"" + value + "\",");
632                                        }
633                                } catch (Exception e) {
634                                        e.printStackTrace();
635                                }
636                        }
637                }
638                Zman zman;
639                Collections.sort(dateList, Zman.DATE_ORDER);
640                for (int i = 0; i < dateList.size(); i++) {
641                        zman = (Zman) dateList.get(i);
642                        sb.append("\t\"").append(zman.getLabel()).append("\":\"");
643                        sb.append(formatter.formatDateTime(zman.getZman(), astronomicalCalendar.getCalendar()));
644                        sb.append("\",\n");
645                }
646                Collections.sort(durationList, Zman.DURATION_ORDER);
647                for (int i = 0; i < durationList.size(); i++) {
648                        zman = (Zman) durationList.get(i);
649                        sb.append("\t\"" + zman.getLabel()).append("\":\"");
650                        sb.append(formatter.format((int) zman.getDuration())).append("\",\n");
651                }
652
653                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
654                        sb.append("\t").append(otherList.get(i)).append("\n");
655                }
656                sb.setLength(sb.length() - 2);
657                sb.append("}\n}");
658                return sb.toString();
659        }
660
661        /**
662         * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
663         * 
664         * @param method the method in question
665         * @return if the method should be included in serialization
666         */
667        private static boolean includeMethod(Method method) {
668                List<String> methodWhiteList = new ArrayList<String>();
669                // methodWhiteList.add("getName");
670
671                List<String> methodBlackList = new ArrayList<String>();
672                // methodBlackList.add("getGregorianChange");
673
674                if (methodWhiteList.contains(method.getName()))
675                        return true;
676                if (methodBlackList.contains(method.getName()))
677                        return false;
678
679                if (method.getParameterTypes().length > 0)
680                        return false; // Skip get methods with parameters since we do not know what value to pass
681                if (!method.getName().startsWith("get"))
682                        return false;
683
684                if (method.getReturnType().getName().endsWith("Date") || method.getReturnType().getName().endsWith("long")) {
685                        return true;
686                }
687                return false;
688        }
689}