001/*
002 * Zmanim Java API
003 * Copyright © 2004-2026 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; version 2.1 of the License.
007 *
008 * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied
009 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
010 * details.
011 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
012 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA,
013 * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
014 */
015package com.kosherjava.zmanim.util;
016
017import java.lang.reflect.Method;
018import java.text.DecimalFormat;
019import java.time.LocalDate;
020import java.time.LocalTime;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Locale;
024import java.time.Duration;
025import java.time.Instant;
026import java.time.ZoneId;
027import java.time.ZonedDateTime;
028import java.time.format.DateTimeFormatter;
029import com.kosherjava.zmanim.AstronomicalCalendar;
030
031/**
032 * A class used to format both {@link Instant} times generated by the Zmanim package as well as {@link Duration} used for temporal
033 * hours / <em>sha'os zmaniyos</em>. For example the {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour()} returns the
034 * length of the hour as a {@code Duration}.
035 * 
036 * @author © Eliyahu Hershfeld 2004 - 2026
037 */
038public class ZmanimFormatter {
039        /**
040         * Setting to prepend a zero to single digit hours.
041         * @see #setSettings(boolean, boolean, boolean)
042         */
043        private boolean prependZeroHours = false;
044
045        /**
046         * Should seconds be used in formatting time.
047         * @see #setSettings(boolean, boolean, boolean)
048         */
049        private boolean useSeconds = false;
050
051        /**
052         * Should milliseconds be used in formatting time.
053         * @see #setSettings(boolean, boolean, boolean)
054         */
055        private boolean useMillis = false;
056
057        /**
058         * the formatter for minutes as seconds.
059         */
060        private static final DecimalFormat minuteSecondNF = new DecimalFormat("00");
061
062        /**
063         * the formatter for hours.
064         */
065        private DecimalFormat hourNF = new DecimalFormat("0");
066
067        /**
068         * the formatter for minutes as milliseconds.
069         */
070        private static final DecimalFormat milliNF = new DecimalFormat("000");
071
072        /**
073         * The {@link DateTimeFormatter} class used by the formatter.
074         * @see #setDateTimeFormatter(DateTimeFormatter)
075         */
076        private DateTimeFormatter dateTimeFormatter;
077        
078        /**
079         * The TimeZone class.
080         * @see #setZoneId(ZoneId)
081         */
082        private ZoneId zoneId = null;
083
084
085        /**
086         * Method to return the {@code ZoneId}.
087         * @return the ZoneId
088         */
089        public ZoneId getZoneId() {
090                return zoneId;
091        }
092
093        /**
094         * Method to set the {@code ZoneId}.
095         * @param zoneId the {@code ZoneId} to set
096         */
097        public void setZoneId(ZoneId zoneId) {
098                this.zoneId = zoneId;
099        }
100
101        /**
102         * Format using hours, minutes, seconds and milliseconds using the xsd:time format. This format will return
103         * {@code 00.00.00.0} when formatting 0.
104         */
105        public static final int SEXAGESIMAL_XSD_FORMAT = 0;
106
107        /**
108         * Set by the constructor; defaults to {@link #SEXAGESIMAL_SECONDS_FORMAT} when using the single-argument constructor.
109         * @see #setTimeFormat(int)
110         */
111        private int timeFormat;
112
113        /** Format using hours and minutes. */
114        public static final int SEXAGESIMAL_FORMAT = 1;
115
116        /** Format using hours, minutes and seconds. */
117        public static final int SEXAGESIMAL_SECONDS_FORMAT = 2;
118
119        /** Format using hours, minutes, seconds and milliseconds. */
120        public static final int SEXAGESIMAL_MILLIS_FORMAT = 3;
121        
122        /**
123         * Format using the XSD Duration format. This is in the format of PT1H6M7.869S (P for period (duration), T for time,
124         * H, M and S indicate hours, minutes and seconds.
125         */
126        public static final int XSD_DURATION_FORMAT = 4;
127
128        /**
129         * Default constructor that uses the format "{@code h:mm:ss}" for dates and "{@code 0:00:00}" for {@link Duration}s.
130         * @param zoneId the {@code ZoneId} {@code Object}.
131         */
132        public ZmanimFormatter(ZoneId zoneId) {
133                this(SEXAGESIMAL_SECONDS_FORMAT, DateTimeFormatter.ofPattern("h:mm:ss"), zoneId);
134        }
135
136        /**
137         * ZmanimFormatter constructor using various formatting options.
138         * 
139         * @param timeFormat The formatting style to use for {@link Duration} used to represent <em>shaos zmaniyos</em>. The formatting
140         *         will be as follows.<ul>
141         *         <li>{@link #SEXAGESIMAL_SECONDS_FORMAT} (the default) - Will format an hour and a half as "{@code 1:30:00}", and half
142         *         an hour as "{@code 0:30:00}".</li>
143         *         <li>{@link #SEXAGESIMAL_XSD_FORMAT} - Will format an hour and a half as "{@code 01:30:00.000}", and half an
144         *         hour as "{@code 00:30:00.000}".</li>
145         *         <li>{@link #SEXAGESIMAL_MILLIS_FORMAT} - Will format an hour and a half as "{@code 1:30:00.000}", and half an hour as
146         *         "{@code 0:30:00.000}".</li>
147         *         <li>{@link #SEXAGESIMAL_FORMAT} will format an hour and a half as "{@code 1:30}", and half an hour as
148         *         "{@code 0:30}".</li>
149         *         <li>{@link #XSD_DURATION_FORMAT} will format the time as : "{@code PT1H30M}", and half an hour as "{@code PT30M}".</li>
150         *         </ul>
151         * @param dateTimeFormatter the {@link DateTimeFormatter} {@code Object}.
152         * @param zoneId the {@code ZoneId} {@code Object}.
153         */
154        public ZmanimFormatter(int timeFormat, DateTimeFormatter dateTimeFormatter, ZoneId zoneId) {
155                setZoneId(zoneId);
156                setTimeFormat(timeFormat);
157                setDateTimeFormatter(dateTimeFormatter.withZone(zoneId));
158        }
159
160        /**
161         * Sets the time format to use for formatting {@link Duration}s.
162         * 
163         * @param format the format constant to use. See {@link #ZmanimFormatter(int, DateTimeFormatter, ZoneId)} for documentation on
164         *         the formats.
165         */
166        public void setTimeFormat(int format) {
167                this.timeFormat = format;
168                switch (format) {
169                case SEXAGESIMAL_XSD_FORMAT:
170                        setSettings(true, true, true);
171                        break;
172                case SEXAGESIMAL_FORMAT:
173                        setSettings(false, false, false);
174                        break;
175                case SEXAGESIMAL_SECONDS_FORMAT:
176                        setSettings(false, true, false);
177                        break;
178                case SEXAGESIMAL_MILLIS_FORMAT:
179                        setSettings(false, true, true);
180                        break;
181                case XSD_DURATION_FORMAT:
182                        break; // the settings are N/A for this 
183                default: throw new IllegalArgumentException("An invalid time format of " + format +
184                                " was set. Please see the documentation for the list of valid formats.");
185                }
186        }
187
188        /**
189         * Sets the {@link DateTimeFormatter} {@code Object}.
190         * @param dateTimeFormatter the {@code DateTimeFormatter} {@code Object} to set.
191         */
192        public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter) {
193                this.dateTimeFormatter = dateTimeFormatter;
194        }
195
196        /**
197         * returns the {@link DateTimeFormatter} {@code Object}.
198         * @return the {@link DateTimeFormatter} {@code Object}.
199         */
200        public DateTimeFormatter getDateTimeFormatter() {
201                return this.dateTimeFormatter;
202        }
203
204        /**
205         * Sets various format settings.
206         * @param prependZeroHours if to prepend a zero for single digit hours (so that 1 o'clock is displayed as 01).
207         * @param useSeconds should seconds be used in the time format.
208         * @param useMillis should milliseconds be used in formatting time.
209         */
210        private void setSettings(boolean prependZeroHours, boolean useSeconds, boolean useMillis) {
211                this.prependZeroHours = prependZeroHours;
212                this.useSeconds = useSeconds;
213                this.useMillis = useMillis;
214                this.hourNF = new DecimalFormat(this.prependZeroHours ? "00" : "0");
215        }
216
217        /**
218         * A method that formats a {@link Duration} into a {@code String} format of "{@code 00:00}", "{@code 00:00:00}" or
219         * "{@code 00:00:00.000}" depending on the settings of the formatter. See the documentation on
220         * {@link ZmanimFormatter#ZmanimFormatter(int, DateTimeFormatter, ZoneId)} for details.
221         * 
222         * @param duration The {@code Duration} to format.
223         * @return String The formatted {@code String}.
224         */
225        public String format(Duration duration) {
226                if (duration == null) {
227                        duration = Duration.ZERO;
228                }
229                
230                if (this.timeFormat == XSD_DURATION_FORMAT) {
231                        return formatXSDDurationTime(duration);
232                }
233                
234                StringBuilder sb = new StringBuilder();
235                if (duration.isNegative()) {
236                        sb.append("-");
237                }
238                
239                Duration absDuration = duration.abs();
240                
241                long hours = absDuration.toHours();
242                long minutes = absDuration.toMinutes() % 60;
243                long seconds = absDuration.toSeconds() % 60;
244                long milliseconds = absDuration.toMillis() % 1000;
245                sb.append(this.hourNF.format(hours));
246                sb.append(":");
247                sb.append(minuteSecondNF.format(minutes));
248                
249                if (this.useSeconds) {
250                        sb.append(":");
251                        sb.append(minuteSecondNF.format(seconds));
252                }
253                if (this.useMillis) {
254                        sb.append(".");
255                        sb.append(milliNF.format(milliseconds));
256                }
257                
258                return sb.toString();
259        }
260
261        /**
262         * Formats an {@link Instant} using this class's {@link #getDateTimeFormatter()}.
263         * 
264         * @param instant the {@code Instant} to format.
265         * @param zoneId the {@link ZoneId} used to help format based on the {@code Instant}'s DST and other settings.
266         * @return the formatted String
267         * @see #formatXSDateTime(Instant)
268         */
269        public String formatInstant(Instant instant, ZoneId zoneId) {
270                ZonedDateTime dateTime = instant.atZone(zoneId);
271                return this.getDateTimeFormatter().format(dateTime);
272        }
273        
274        /**
275         * Format the {@code Instant} using the format "{@code yyyy-MM-dd'T'HH:mm:ssXXX}".
276         * @param instant the {@code Instant} to format.
277         * @return the {@code Instant} formatted using the format "{@code yyyy-MM-dd'T'HH:mm:ssXXX}".
278         */
279        public String formatXSDateTime(Instant instant) {
280                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").withZone(getZoneId());
281                return formatter.format(instant);
282        }
283
284        /**
285         * This returns the xml representation of an xsd:duration object. This simply uses {@link Duration#toString()} that formats it
286         * in the standard XSD ISO-8601 (e.g., "{@code PT1H30M}")
287         * 
288         * @param duration the duration to format
289         * @return the xsd:duration ISO-8601 formatted String
290         */
291        public String formatXSDDurationTime(Duration duration) {
292                if (duration == null) {
293                        return "";
294                }
295                return duration.toString(); // Java's native toString() outputs a standard XSD ISO-8601 (e.g., "PT1H30M")
296        }
297
298        /**
299         * A method that returns an XML formatted {@code String} representing the serialized {@code Object}. The
300         * format used is:
301         * 
302         * {@snippet lang='xml' :
303         *  <AstronomicalTimes date="1969-02-08" type="com.kosherjava.zmanim.AstronomicalCalendar"
304         *         algorithm="US Naval Almanac Algorithm" location="Montreal, Quebec" latitude="45.497" longitude="-73.63"
305         *         elevation="85.0" timeZoneName="Eastern Standard Time" timeZoneID="America/New_York" timeZoneOffset="-5">
306         *     <SeaLevelSunset>1969-02-08T17:11:26-05:00</SeaLevelSunset>
307         *     <TemporalHour>PT50M23.259S</TemporalHour>
308         *     ...
309         *   </AstronomicalTimes>
310         * }
311         * 
312         * If a zman does not occur, the value "N/A" will be returned.
313         * 
314         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
315         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
316         * times that are a duration such as the length of a
317         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
318         * returned by the {@link #toString() toString}.
319         * 
320         * @param astronomicalCalendar the AstronomicalCalendar Object
321         * 
322         * @return The XML {@code String} formatted as described above.
323         * 
324         * @todo Add proper schema, and support for nulls. XSD duration (for solar hours), should probably return nil and not P.
325         */
326        public static String toXML(AstronomicalCalendar astronomicalCalendar) {
327                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, DateTimeFormatter.ofPattern(
328                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getZoneId());
329                DateTimeFormatter xsdFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
330                                .withZone(astronomicalCalendar.getGeoLocation().getZoneId());
331                DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd");
332                df = df.withZone(astronomicalCalendar.getGeoLocation().getZoneId());
333
334                LocalDate localDate = astronomicalCalendar.getLocalDate();
335                GeoLocation geoLocation = astronomicalCalendar.getGeoLocation();
336                ZonedDateTime lastMidnight = ZonedDateTime.of(astronomicalCalendar.getLocalDate(), LocalTime.MIDNIGHT, astronomicalCalendar.getGeoLocation().getZoneId());
337                double offsetHours = lastMidnight.getOffset().getTotalSeconds() / 3600.0;
338                String timeZoneName = lastMidnight.format(DateTimeFormatter.ofPattern("zzzz", Locale.getDefault()));
339
340                StringBuilder sb = new StringBuilder("<");
341                boolean isAstronomicalCalendar = astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.AstronomicalCalendar");
342                boolean isComprehensiveZmanimCalendar = astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ComprehensiveZmanimCalendar");
343                boolean isZmanimCalendar = astronomicalCalendar.getClass().getName().equals("com.kosherjava.zmanim.ZmanimCalendar");
344                if (isAstronomicalCalendar) {
345                        sb.append("AstronomicalTimes");
346                } else if (isComprehensiveZmanimCalendar) {
347                        sb.append("Zmanim");
348                } else if (isZmanimCalendar) {
349                        sb.append("BasicZmanim");
350                }
351                sb.append(" date=\"").append(df.format(localDate)).append("\"");
352                sb.append(" type=\"").append(astronomicalCalendar.getClass().getName()).append("\"");
353                sb.append(" algorithm=\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\"");
354                sb.append(" location=\"").append(astronomicalCalendar.getGeoLocation().getLocationName()).append("\"");
355                sb.append(" latitude=\"").append(astronomicalCalendar.getGeoLocation().getLatitude()).append("\"");
356                sb.append(" longitude=\"").append(astronomicalCalendar.getGeoLocation().getLongitude()).append("\"");
357                sb.append(" elevation=\"").append(astronomicalCalendar.getGeoLocation().getElevation()).append("\"");
358                sb.append(" timeZoneName=\"").append(timeZoneName).append("\"");
359                sb.append(" timeZoneID=\"").append(geoLocation.getZoneId().getId()).append("\"");
360                sb.append(" timeZoneOffset=\"").append(offsetHours).append("\"");
361                //sb.append(" useElevationAllZmanim=\"").append(astronomicalCalendar.useElevationAllZmanim()).append("\""); //TODO likely using reflection
362                sb.append(">\n");
363
364                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
365                String tagName;
366                Object value;
367                List<Zman> dateList = new ArrayList<>();
368                List<Zman> durationList = new ArrayList<>();
369                List<String> otherList = new ArrayList<>();
370                for (Method theMethod : theMethods) {
371                        if (includeMethod(theMethod)) {
372                                tagName = theMethod.getName().substring(3);
373                                // String returnType = theMethods[i].getReturnType().getName();
374                                try {
375                                        value = theMethod.invoke(astronomicalCalendar, (Object[]) null);
376                                        if (value == null) {
377                                                otherList.add("<" + tagName + ">N/A</" + tagName + ">");
378                                        } else if (value instanceof Instant) {
379                                                dateList.add(new Zman((Instant) value, tagName));
380                                        } else if (value instanceof Duration) {// shaah zmanis
381                                                durationList.add(new Zman((Duration)value, tagName));
382                                        } else { // will probably never enter this block, but is present to be future-proof
383                                                otherList.add("<" + tagName + ">" + value + "</" + tagName + ">");
384                                        }
385                                } catch (Exception e) {
386                                        e.printStackTrace();
387                                }
388                        }
389                }
390                Zman zman;
391                dateList.sort(Zman.DATE_ORDER);
392
393                for (int i = 0; i < dateList.size(); i++) {
394                        zman = dateList.get(i);
395                        sb.append("\t<").append(zman.getLabel()).append(">");
396                        sb.append(xsdFormatter.format(zman.getZman()));
397                        sb.append("</").append(zman.getLabel()).append(">\n");
398                }
399                durationList.sort(Zman.DURATION_ORDER);
400                for (int i = 0; i < durationList.size(); i++) {
401                        zman = durationList.get(i);
402                        sb.append("\t<" + zman.getLabel()).append(">");
403                        sb.append(formatter.format(zman.getDuration())).append("</").append(zman.getLabel())
404                                        .append(">\n");
405                }
406
407                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
408                        sb.append("\t").append(otherList.get(i)).append("\n");
409                }
410
411                if (isAstronomicalCalendar) {
412                        sb.append("</AstronomicalTimes>");
413                } else if (isComprehensiveZmanimCalendar) {
414                        sb.append("</Zmanim>");
415                } else if (isZmanimCalendar) {
416                        sb.append("</BasicZmanim>");
417                }
418                return sb.toString();
419        }
420        
421        /**
422         * A method that returns a JSON formatted {@code String} representing the serialized {@code Object}. The format used is:
423         * {@snippet lang='json' :
424         * {
425         *    "metadata":{
426         *      "date":"1969-02-08",
427         *      "type":"com.kosherjava.zmanim.AstronomicalCalendar",
428         *      "algorithm":"US Naval Almanac Algorithm",
429         *      "location":"Montreal, Quebec",
430         *      "latitude":"45.497",
431         *      "longitude":"-73.63",
432         *      "elevation":"85.0",
433         *      "timeZoneName":"Eastern Standard Time",
434         *      "timeZoneID":"America/New_York",
435         *      "timeZoneOffset":"-5"},
436         *    "AstronomicalTimes":{
437         *     "SeaLevelSunset":"1969-02-08T17:11:26-05:00",
438         *     "TemporalHour":"PT50M23.259S"
439         *     ...
440         *     }
441         * }
442         * }
443         * 
444         * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for
445         * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for
446         * times that are a duration such as the length of a
447         * {@link com.kosherjava.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}.
448         * If a zman does not occur, the value "N/A" will be returned.
449         * 
450         * @param astronomicalCalendar the AstronomicalCalendar Object
451         * 
452         * @return The JSON {@code String} formatted as described above.
453         */
454        public static String toJSON(AstronomicalCalendar astronomicalCalendar) {
455                ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, DateTimeFormatter.ofPattern(
456                                "yyyy-MM-dd'T'HH:mm:ss"), astronomicalCalendar.getGeoLocation().getZoneId());
457                DateTimeFormatter xsdFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
458                                .withZone(astronomicalCalendar.getGeoLocation().getZoneId());
459                DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd")
460                                .withZone(astronomicalCalendar.getGeoLocation().getZoneId());
461
462                LocalDate localDate = astronomicalCalendar.getLocalDate();
463                GeoLocation geoLocation = astronomicalCalendar.getGeoLocation();                
464                ZonedDateTime lastMidnight = ZonedDateTime.of(astronomicalCalendar.getLocalDate(), LocalTime.MIDNIGHT,
465                                astronomicalCalendar.getGeoLocation().getZoneId());
466                double offsetHours = lastMidnight.getOffset().getTotalSeconds() / 3600.0;
467                String timeZoneName = lastMidnight.format(DateTimeFormatter.ofPattern("zzzz", Locale.getDefault()));
468
469                StringBuilder sb = new StringBuilder("{\n\"metadata\":{\n");
470                sb.append("\t\"date\":\"").append(df.format(localDate)).append("\",\n");
471                sb.append("\t\"type\":\"").append(astronomicalCalendar.getClass().getName()).append("\",\n");
472                sb.append("\t\"algorithm\":\"").append(astronomicalCalendar.getAstronomicalCalculator().getCalculatorName()).append("\",\n");
473                sb.append("\t\"location\":\"").append(geoLocation.getLocationName()).append("\",\n");
474                sb.append("\t\"latitude\":\"").append(geoLocation.getLatitude()).append("\",\n");
475                sb.append("\t\"longitude\":\"").append(geoLocation.getLongitude()).append("\",\n");
476                sb.append("\t\"elevation\":\"").append(geoLocation.getElevation()).append("\",\n");
477                sb.append("\t\"timeZoneName\":\"").append(timeZoneName).append("\",\n");
478                sb.append("\t\"timeZoneID\":\"").append(geoLocation.getZoneId().getId()).append("\",\n");
479                sb.append("\t\"timeZoneOffset\":\"").append(offsetHours).append("\"");
480                sb.append("},\n\"");
481
482                switch (astronomicalCalendar.getClass().getName()) {
483                        case "com.kosherjava.zmanim.AstronomicalCalendar":
484                                sb.append("AstronomicalTimes");
485                                break;
486                        case "com.kosherjava.zmanim.ComprehensiveZmanimCalendar":
487                                sb.append("Zmanim");
488                                break;
489                        case "com.kosherjava.zmanim.ZmanimCalendar":
490                                sb.append("BasicZmanim");
491                                break;
492                }
493                sb.append("\":{\n");
494                Method[] theMethods = astronomicalCalendar.getClass().getMethods();
495                String tagName;
496                Object value;
497                List<Zman> dateList = new ArrayList<>();
498                List<Zman> durationList = new ArrayList<>();
499                List<String> otherList = new ArrayList<>();
500                for (Method theMethod : theMethods) {
501                        if (includeMethod(theMethod)) {
502                                tagName = theMethod.getName().substring(3);
503                                // String returnType = theMethods[i].getReturnType().getName();
504                                try {
505                                        value = theMethod.invoke(astronomicalCalendar, (Object[]) null);
506                                        if (value == null) {
507                                                otherList.add("\"" + tagName + "\":\"N/A\",");
508                                        } else if (value instanceof Instant) {
509                                                dateList.add(new Zman((Instant) value, tagName));
510                                        } else if (value instanceof Duration) {// shaah zmanis
511                                                durationList.add(new Zman((Duration)value, tagName));
512                                        } else { // will probably never enter this block, but is present to be future-proof
513                                                otherList.add("\"" + tagName + "\":\"" + value + "\",");
514                                        }
515                                } catch (Exception e) {
516                                        e.printStackTrace();
517                                }
518                        }
519                }
520                Zman zman;
521                dateList.sort(Zman.DATE_ORDER);
522                for (int i = 0; i < dateList.size(); i++) {
523                        zman = dateList.get(i);
524                        sb.append("\t\"").append(zman.getLabel()).append("\":\"");
525                        sb.append(xsdFormatter.format(zman.getZman()));
526                        sb.append("\",\n");
527                }
528                durationList.sort(Zman.DURATION_ORDER);
529                for (int i = 0; i < durationList.size(); i++) {
530                        zman = durationList.get(i);
531                        sb.append("\t\"" + zman.getLabel()).append("\":\"");
532                        sb.append(formatter.format(zman.getDuration())).append("\",\n");
533                }
534
535                for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block
536                        sb.append("\t").append(otherList.get(i)).append("\n");
537                }
538                sb.setLength(sb.length() - 2);
539                sb.append("}\n}");
540                return sb.toString();
541        }
542
543        /**
544         * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
545         * 
546         * @param method the method in question
547         * @return if the method should be included in serialization
548         */
549        private static boolean includeMethod(Method method) {
550                if (method.getParameterTypes().length > 0)
551                        return false; // Skip get methods with parameters since we do not know what value to pass
552                if (!method.getName().startsWith("get"))
553                        return false;
554
555                return method.getReturnType().getName().endsWith("Instant") || method.getReturnType().getName().endsWith("Duration");
556        }
557}