001/*
002 * Zmanim Java API
003 * Copyright (C) 2004-2024 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 - 2024
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); FIXME
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 that is now ignored.
301         * @return the XSD dateTime
302         * @deprecated This method will be removed in v3.0
303         */
304        @Deprecated // (since="2.5", forRemoval=true) // add back once Java 9 is the minimum supported version
305        public String getXSDateTime(Date date, Calendar calendar) {
306                return getXSDateTime(date);
307        }
308        
309        public String getXSDateTime(Date date) {
310                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
311                dateFormat.setTimeZone(getTimeZone());
312                return new StringBuilder(dateFormat.format(date)).toString();
313        }
314
315        /**
316         * Represent the hours and minutes with two-digit strings.
317         * 
318         * @param digits
319         *            hours or minutes.
320         * @return two-digit String representation of hrs or minutes.
321         */
322        private static String formatDigits(int digits) {
323                String dd = String.valueOf(Math.abs(digits));
324                return dd.length() == 1 ? '0' + dd : dd;
325        }
326
327        /**
328         * This returns the xml representation of an xsd:duration object.
329         * 
330         * @param millis
331         *            the duration in milliseconds
332         * @return the xsd:duration formatted String
333         */
334        public String formatXSDDurationTime(long millis) {
335                return formatXSDDurationTime(new Time(millis));
336        }
337
338        /**
339         * This returns the xml representation of an xsd:duration object.
340         * 
341         * @param time
342         *            the duration as a Time object
343         * @return the xsd:duration formatted String
344         */
345        public String formatXSDDurationTime(Time time) {
346                StringBuilder duration = new StringBuilder();
347                if (time.getHours() != 0 || time.getMinutes() != 0 || time.getSeconds() != 0 || time.getMilliseconds() != 0) {
348                        duration.append("P");
349                        duration.append("T");
350
351                        if (time.getHours() != 0)
352                                duration.append(time.getHours() + "H");
353
354                        if (time.getMinutes() != 0)
355                                duration.append(time.getMinutes() + "M");
356
357                        if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
358                                duration.append(time.getSeconds() + "." + milliNF.format(time.getMilliseconds()));
359                                duration.append("S");
360                        }
361                        if (duration.length() == 1) // zero seconds
362                                duration.append("T0S");
363                        if (time.isNegative())
364                                duration.insert(0, "-");
365                }
366                return duration.toString();
367        }
368
369        /**
370         * A method that returns an XML formatted <code>String</code> representing the serialized <code>Object</code>. The
371         * format used is:
372         * 
373         * <pre>
374         *  &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;
375         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
376         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
377         *     ...
378         *   &lt;/AstronomicalTimes&gt;
379         * </pre>
380         * 
381         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
382         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
383         * times that are a duration such as the length of a
384         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
385         * returned by the {@link #toString() toString}.
386         * 
387         * @param astronomicalCalendar the AstronomicalCalendar Object
388         * 
389         * @return The XML formatted <code>String</code>. The format will be:
390         * 
391         *         <pre>
392         *  &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;
393         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
394         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
395         *     ...
396         *  &lt;/AstronomicalTimes&gt;
397         * </pre>
398         * 
399         * @todo Add proper schema, and support for nulls. XSD duration (for solar hours), should probably return nil and not P.
400         */
401        public static String toXML(AstronomicalCalendar astronomicalCalendar) {
402                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
403                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getTimeZone());
404                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
405                df.setTimeZone(astronomicalCalendar.getGeoLocation().getTimeZone());
406
407                Date date = astronomicalCalendar.getCalendar().getTime();
408                TimeZone tz = astronomicalCalendar.getGeoLocation().getTimeZone();
409                boolean daylight = tz.useDaylightTime() && tz.inDaylightTime(date);
410
411                StringBuilder sb = new StringBuilder("<");
412                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
413                        sb.append("AstronomicalTimes");
414                        // TODO: use proper schema ref, and maybe build a real schema.
415                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
416                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim astronomical.xsd"
417                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
418                        sb.append("Zmanim");
419                        // TODO: use proper schema ref, and maybe build a real schema.
420                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
421                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim zmanim.xsd"
422                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
423                        sb.append("BasicZmanim");
424                        // TODO: use proper schema ref, and maybe build a real schema.
425                        // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
426                        // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim basicZmanim.xsd"
427                }
428                sb.append(" date=\"").append(df.format(date)).append("\"");
429                sb.append(" type=\"").append(astronomicalCalendar.getClass().getName()).append("\"");
430                sb.append(" algorithm=\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\"");
431                sb.append(" location=\"").append(astronomicalCalendar.getGeoLocation().getLocationName()).append("\"");
432                sb.append(" latitude=\"").append(astronomicalCalendar.getGeoLocation().getLatitude()).append("\"");
433                sb.append(" longitude=\"").append(astronomicalCalendar.getGeoLocation().getLongitude()).append("\"");
434                sb.append(" elevation=\"").append(astronomicalCalendar.getGeoLocation().getElevation()).append("\"");
435                sb.append(" timeZoneName=\"").append(tz.getDisplayName(daylight, TimeZone.LONG)).append("\"");
436                sb.append(" timeZoneID=\"").append(tz.getID()).append("\"");
437                sb.append(" timeZoneOffset=\"")
438                                .append((tz.getOffset(astronomicalCalendar.getCalendar().getTimeInMillis()) / ((double) HOUR_MILLIS)))
439                                .append("\"");
440                //TODO user reflection for the following
441                //sb.append(" useElevationAllZmanim=\"").append(astronomicalCalendar.getuse).append("\"");
442
443                sb.append(">\n");
444
445                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
446                String tagName = "";
447                Object value = null;
448                List<Zman> dateList = new ArrayList<Zman>();
449                List<Zman> durationList = new ArrayList<Zman>();
450                List<String> otherList = new ArrayList<String>();
451                for (int i = 0; i < theMethods.length; i++) {
452                        if (includeMethod(theMethods[i])) {
453                                tagName = theMethods[i].getName().substring(3);
454                                // String returnType = theMethods[i].getReturnType().getName();
455                                try {
456                                        value = theMethods[i].invoke(astronomicalCalendar, (Object[]) null);
457                                        if (value == null) {// TODO: Consider using reflection to determine the return type, not the 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 if (value instanceof Date) {
462                                                dateList.add(new Zman((Date) value, tagName));
463                                        } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis
464                                                if (((Long) value).longValue() == Long.MIN_VALUE) {
465                                                        otherList.add("<" + tagName + ">N/A</" + tagName + ">");
466                                                        // TODO: instead of N/A, consider return proper xs:nil.
467                                                        // otherList.add("<" + tagName + " xs:nil=\"true\" />");
468                                                } else {
469                                                        durationList.add(new Zman((int) ((Long) value).longValue(), tagName));
470                                                }
471                                        } else { // will probably never enter this block, but is present to be future proof
472                                                otherList.add("<" + tagName + ">" + value + "</" + tagName + ">");
473                                        }
474                                } catch (Exception e) {
475                                        e.printStackTrace();
476                                }
477                        }
478                }
479                Zman zman;
480                Collections.sort(dateList, Zman.DATE_ORDER);
481
482                for (int i = 0; i < dateList.size(); i++) {
483                        zman = (Zman) dateList.get(i);
484                        sb.append("\t<").append(zman.getLabel()).append(">");
485                        sb.append(formatter.formatDateTime(zman.getZman(), astronomicalCalendar.getCalendar()));
486                        sb.append("</").append(zman.getLabel()).append(">\n");
487                }
488                Collections.sort(durationList, Zman.DURATION_ORDER);
489                for (int i = 0; i < durationList.size(); i++) {
490                        zman = (Zman) durationList.get(i);
491                        sb.append("\t<" + zman.getLabel()).append(">");
492                        sb.append(formatter.format((int) zman.getDuration())).append("</").append(zman.getLabel())
493                                        .append(">\n");
494                }
495
496                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
497                        sb.append("\t").append(otherList.get(i)).append("\n");
498                }
499
500                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
501                        sb.append("</AstronomicalTimes>");
502                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
503                        sb.append("</Zmanim>");
504                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
505                        sb.append("</BasicZmanim>");
506                }
507                return sb.toString();
508        }
509        
510        /**
511         * A method that returns a JSON formatted <code>String</code> representing the serialized <code>Object</code>. The
512         * format used is:
513         * <pre>
514         * {
515         *    &quot;metadata&quot;:{
516         *      &quot;date&quot;:&quot;1969-02-08&quot;,
517         *      &quot;type&quot;:&quot;com.kosherjava.zmanim.AstronomicalCalendar&quot;,
518         *      &quot;algorithm&quot;:&quot;US Naval Almanac Algorithm&quot;,
519         *      &quot;location&quot;:&quot;Lakewood, NJ&quot;,
520         *      &quot;latitude&quot;:&quot;40.095965&quot;,
521         *      &quot;longitude&quot;:&quot;-74.22213&quot;,
522         *      &quot;elevation:&quot;31.0&quot;,
523         *      &quot;timeZoneName&quot;:&quot;Eastern Standard Time&quot;,
524         *      &quot;timeZoneID&quot;:&quot;America/New_York&quot;,
525         *      &quot;timeZoneOffset&quot;:&quot;-5&quot;},
526         *    &quot;AstronomicalTimes&quot;:{
527         *     &quot;Sunrise&quot;:&quot;2007-02-18T06:45:27-05:00&quot;,
528         *     &quot;TemporalHour&quot;:&quot;PT54M17.529S&quot;
529         *     ...
530         *     }
531         * }
532         * </pre>
533         * 
534         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
535         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
536         * times that are a duration such as the length of a
537         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}.
538         * 
539         * @param astronomicalCalendar the AstronomicalCalendar Object
540         * 
541         * @return The JSON formatted <code>String</code>. The format will be:
542         * <pre>
543         * {
544         *    &quot;metadata&quot;:{
545         *      &quot;date&quot;:&quot;1969-02-08&quot;,
546         *      &quot;type&quot;:&quot;com.kosherjava.zmanim.AstronomicalCalendar&quot;,
547         *      &quot;algorithm&quot;:&quot;US Naval Almanac Algorithm&quot;,
548         *      &quot;location&quot;:&quot;Lakewood, NJ&quot;,
549         *      &quot;latitude&quot;:&quot;40.095965&quot;,
550         *      &quot;longitude&quot;:&quot;-74.22213&quot;,
551         *      &quot;elevation:&quot;31.0&quot;,
552         *      &quot;timeZoneName&quot;:&quot;Eastern Standard Time&quot;,
553         *      &quot;timeZoneID&quot;:&quot;America/New_York&quot;,
554         *      &quot;timeZoneOffset&quot;:&quot;-5&quot;},
555         *    &quot;AstronomicalTimes&quot;:{
556         *     &quot;Sunrise&quot;:&quot;2007-02-18T06:45:27-05:00&quot;,
557         *     &quot;TemporalHour&quot;:&quot;PT54M17.529S&quot;
558         *     ...
559         *     }
560         * }
561         * </pre>
562         */
563        public static String toJSON(AstronomicalCalendar astronomicalCalendar) {
564                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
565                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getTimeZone());
566                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
567                df.setTimeZone(astronomicalCalendar.getGeoLocation().getTimeZone());
568
569                Date date = astronomicalCalendar.getCalendar().getTime();
570                TimeZone tz = astronomicalCalendar.getGeoLocation().getTimeZone();
571                boolean daylight = tz.useDaylightTime() && tz.inDaylightTime(date);
572
573                StringBuilder sb = new StringBuilder("{\n\"metadata\":{\n");
574                sb.append("\t\"date\":\"").append(df.format(date)).append("\",\n");
575                sb.append("\t\"type\":\"").append(astronomicalCalendar.getClass().getName()).append("\",\n");
576                sb.append("\t\"algorithm\":\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\",\n");
577                sb.append("\t\"location\":\"").append(astronomicalCalendar.getGeoLocation().getLocationName()).append("\",\n");
578                sb.append("\t\"latitude\":\"").append(astronomicalCalendar.getGeoLocation().getLatitude()).append("\",\n");
579                sb.append("\t\"longitude\":\"").append(astronomicalCalendar.getGeoLocation().getLongitude()).append("\",\n");
580                sb.append("\t\"elevation\":\"").append(astronomicalCalendar.getGeoLocation().getElevation()).append("\",\n");
581                sb.append("\t\"timeZoneName\":\"").append(tz.getDisplayName(daylight, TimeZone.LONG)).append("\",\n");
582                sb.append("\t\"timeZoneID\":\"").append(tz.getID()).append("\",\n");
583                sb.append("\t\"timeZoneOffset\":\"")
584                                .append((tz.getOffset(astronomicalCalendar.getCalendar().getTimeInMillis()) / ((double) HOUR_MILLIS)))
585                                .append("\"");
586
587                sb.append("},\n\"");
588                
589                if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar")) {
590                        sb.append("AstronomicalTimes");
591                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComplexZmanimCalendar")) {
592                        sb.append("Zmanim");
593                } else if (astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar")) {
594                        sb.append("BasicZmanim");
595                }
596                sb.append("\":{\n");
597                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
598                String tagName = "";
599                Object value = null;
600                List<Zman> dateList = new ArrayList<Zman>();
601                List<Zman> durationList = new ArrayList<Zman>();
602                List<String> otherList = new ArrayList<String>();
603                for (int i = 0; i < theMethods.length; i++) {
604                        if (includeMethod(theMethods[i])) {
605                                tagName = theMethods[i].getName().substring(3);
606                                // String returnType = theMethods[i].getReturnType().getName();
607                                try {
608                                        value = theMethods[i].invoke(astronomicalCalendar, (Object[]) null);
609                                        if (value == null) {// TODO: Consider using reflection to determine the return type, not the value
610                                                otherList.add("\"" + tagName + "\":\"N/A\",");
611                                        } else if (value instanceof Date) {
612                                                dateList.add(new Zman((Date) value, tagName));
613                                        } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis
614                                                if (((Long) value).longValue() == Long.MIN_VALUE) {
615                                                        otherList.add("\"" + tagName + "\":\"N/A\"");
616                                                } else {
617                                                        durationList.add(new Zman((int) ((Long) value).longValue(), tagName));
618                                                }
619                                        } else { // will probably never enter this block, but is present to be future proof
620                                                otherList.add("\"" + tagName + "\":\"" + value + "\",");
621                                        }
622                                } catch (Exception e) {
623                                        e.printStackTrace();
624                                }
625                        }
626                }
627                Zman zman;
628                Collections.sort(dateList, Zman.DATE_ORDER);
629                for (int i = 0; i < dateList.size(); i++) {
630                        zman = (Zman) dateList.get(i);
631                        sb.append("\t\"").append(zman.getLabel()).append("\":\"");
632                        sb.append(formatter.formatDateTime(zman.getZman(), astronomicalCalendar.getCalendar()));
633                        sb.append("\",\n");
634                }
635                Collections.sort(durationList, Zman.DURATION_ORDER);
636                for (int i = 0; i < durationList.size(); i++) {
637                        zman = (Zman) durationList.get(i);
638                        sb.append("\t\"" + zman.getLabel()).append("\":\"");
639                        sb.append(formatter.format((int) zman.getDuration())).append("\",\n");
640                }
641
642                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
643                        sb.append("\t").append(otherList.get(i)).append("\n");
644                }
645                sb.setLength(sb.length() - 2);
646                sb.append("}\n}");
647                return sb.toString();
648        }
649
650        /**
651         * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
652         * 
653         * @param method the method in question
654         * @return if the method should be included in serialization
655         */
656        private static boolean includeMethod(Method method) {
657                List<String> methodWhiteList = new ArrayList<String>();
658                // methodWhiteList.add("getName");
659
660                List<String> methodBlackList = new ArrayList<String>();
661                // methodBlackList.add("getGregorianChange");
662
663                if (methodWhiteList.contains(method.getName()))
664                        return true;
665                if (methodBlackList.contains(method.getName()))
666                        return false;
667
668                if (method.getParameterTypes().length > 0)
669                        return false; // Skip get methods with parameters since we do not know what value to pass
670                if (!method.getName().startsWith("get"))
671                        return false;
672
673                if (method.getReturnType().getName().endsWith("Date") || method.getReturnType().getName().endsWith("long")) {
674                        return true;
675                }
676                return false;
677        }
678}