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 * <AstronomicalTimes date="1969-02-08" type="com.kosherjava.zmanim.AstronomicalCalendar algorithm="US Naval Almanac Algorithm" location="Lakewood, NJ" latitude="40.095965" longitude="-74.22213" elevation="31.0" timeZoneName="Eastern Standard Time" timeZoneID="America/New_York" timeZoneOffset="-5"> 375 * <Sunrise>2007-02-18T06:45:27-05:00</Sunrise> 376 * <TemporalHour>PT54M17.529S</TemporalHour> 377 * ... 378 * </AstronomicalTimes> 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 * <AstronomicalTimes date="1969-02-08" type="com.kosherjava.zmanim.AstronomicalCalendar algorithm="US Naval Almanac Algorithm" location="Lakewood, NJ" latitude="40.095965" longitude="-74.22213" elevation="31.0" timeZoneName="Eastern Standard Time" timeZoneID="America/New_York" timeZoneOffset="-5"> 393 * <Sunrise>2007-02-18T06:45:27-05:00</Sunrise> 394 * <TemporalHour>PT54M17.529S</TemporalHour> 395 * ... 396 * </AstronomicalTimes> 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 * "metadata":{ 516 * "date":"1969-02-08", 517 * "type":"com.kosherjava.zmanim.AstronomicalCalendar", 518 * "algorithm":"US Naval Almanac Algorithm", 519 * "location":"Lakewood, NJ", 520 * "latitude":"40.095965", 521 * "longitude":"-74.22213", 522 * "elevation:"31.0", 523 * "timeZoneName":"Eastern Standard Time", 524 * "timeZoneID":"America/New_York", 525 * "timeZoneOffset":"-5"}, 526 * "AstronomicalTimes":{ 527 * "Sunrise":"2007-02-18T06:45:27-05:00", 528 * "TemporalHour":"PT54M17.529S" 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 * "metadata":{ 545 * "date":"1969-02-08", 546 * "type":"com.kosherjava.zmanim.AstronomicalCalendar", 547 * "algorithm":"US Naval Almanac Algorithm", 548 * "location":"Lakewood, NJ", 549 * "latitude":"40.095965", 550 * "longitude":"-74.22213", 551 * "elevation:"31.0", 552 * "timeZoneName":"Eastern Standard Time", 553 * "timeZoneID":"America/New_York", 554 * "timeZoneOffset":"-5"}, 555 * "AstronomicalTimes":{ 556 * "Sunrise":"2007-02-18T06:45:27-05:00", 557 * "TemporalHour":"PT54M17.529S" 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}