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