001/* 002 * Zmanim Java API 003 * Copyright (C) 2004-2011 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: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html 015 */ 016package net.sourceforge.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 net.sourceforge.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 net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour()} returns the length of the hour in 033 * milliseconds. This class can format this time. 034 * 035 * @author © Eliyahu Hershfeld 2004 - 2011 036 * @version 1.2 037 */ 038public class ZmanimFormatter { 039 private boolean prependZeroHours = false; 040 041 private boolean useSeconds = false; 042 043 private boolean useMillis = false; 044 045 private static DecimalFormat minuteSecondNF = new DecimalFormat("00"); 046 047 private DecimalFormat hourNF; 048 049 private static DecimalFormat milliNF = new DecimalFormat("000"); 050 051 private SimpleDateFormat dateFormat; 052 053 private TimeZone timeZone = null; // TimeZone.getTimeZone("UTC"); 054 055 // private DecimalFormat decimalNF; 056 057 /** 058 * @return the timeZone 059 */ 060 public TimeZone getTimeZone() { 061 return timeZone; 062 } 063 064 /** 065 * @param timeZone 066 * the timeZone to set 067 */ 068 public void setTimeZone(TimeZone timeZone) { 069 this.timeZone = timeZone; 070 } 071 072 /** 073 * Format using hours, minutes, seconds and milliseconds using the xsd:time format. This format will return 074 * 00.00.00.0 when formatting 0. 075 */ 076 public static final int SEXAGESIMAL_XSD_FORMAT = 0; 077 078 private int timeFormat = SEXAGESIMAL_XSD_FORMAT; 079 080 /** 081 * Format using standard decimal format with 5 positions after the decimal. 082 */ 083 public static final int DECIMAL_FORMAT = 1; 084 085 /** Format using hours and minutes. */ 086 public static final int SEXAGESIMAL_FORMAT = 2; 087 088 /** Format using hours, minutes and seconds. */ 089 public static final int SEXAGESIMAL_SECONDS_FORMAT = 3; 090 091 /** Format using hours, minutes, seconds and milliseconds. */ 092 public static final int SEXAGESIMAL_MILLIS_FORMAT = 4; 093 094 /** constant for milliseconds in a minute (60,000) */ 095 static final long MINUTE_MILLIS = 60 * 1000; 096 097 /** constant for milliseconds in an hour (3,600,000) */ 098 public static final long HOUR_MILLIS = MINUTE_MILLIS * 60; 099 100 /** 101 * Format using the XSD Duration format. This is in the format of PT1H6M7.869S (P for period (duration), T for time, 102 * H, M and S indicate hours, minutes and seconds. 103 */ 104 public static final int XSD_DURATION_FORMAT = 5; 105 106 /** 107 * constructor that defaults to this will use the format "h:mm:ss" for dates and 00.00.00.0 for {@link Time}. 108 */ 109 public ZmanimFormatter(TimeZone timeZone) { 110 this(0, new SimpleDateFormat("h:mm:ss"), timeZone); 111 } 112 113 // public ZmanimFormatter() { 114 // this(0, new SimpleDateFormat("h:mm:ss"), TimeZone.getTimeZone("UTC")); 115 // } 116 117 /** 118 * ZmanimFormatter constructor using a formatter 119 * 120 * @param format 121 * int The formatting style to use. Using ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the time 122 * time of 90*60*1000 + 1 as 1:30:00 123 */ 124 public ZmanimFormatter(int format, SimpleDateFormat dateFormat, TimeZone timeZone) { 125 setTimeZone(timeZone); 126 String hourFormat = "0"; 127 if (prependZeroHours) { 128 hourFormat = "00"; 129 } 130 this.hourNF = new DecimalFormat(hourFormat); 131 setTimeFormat(format); 132 dateFormat.setTimeZone(timeZone); 133 setDateFormat(dateFormat); 134 } 135 136 /** 137 * Sets the format to use for formatting. 138 * 139 * @param format 140 * int the format constant to use. 141 */ 142 public void setTimeFormat(int format) { 143 this.timeFormat = format; 144 switch (format) { 145 case SEXAGESIMAL_XSD_FORMAT: 146 setSettings(true, true, true); 147 break; 148 case SEXAGESIMAL_FORMAT: 149 setSettings(false, false, false); 150 break; 151 case SEXAGESIMAL_SECONDS_FORMAT: 152 setSettings(false, true, false); 153 break; 154 case SEXAGESIMAL_MILLIS_FORMAT: 155 setSettings(false, true, true); 156 break; 157 // case DECIMAL_FORMAT: 158 // default: 159 } 160 } 161 162 public void setDateFormat(SimpleDateFormat sdf) { 163 this.dateFormat = sdf; 164 } 165 166 public SimpleDateFormat getDateFormat() { 167 return this.dateFormat; 168 } 169 170 private void setSettings(boolean prependZeroHours, boolean useSeconds, boolean useMillis) { 171 this.prependZeroHours = prependZeroHours; 172 this.useSeconds = useSeconds; 173 this.useMillis = useMillis; 174 } 175 176 /** 177 * A method that formats milliseconds into a time format. 178 * 179 * @param milliseconds 180 * The time in milliseconds. 181 * @return String The formatted <code>String</code> 182 */ 183 public String format(double milliseconds) { 184 return format((int) milliseconds); 185 } 186 187 /** 188 * A method that formats milliseconds into a time format. 189 * 190 * @param millis 191 * The time in milliseconds. 192 * @return String The formatted <code>String</code> 193 */ 194 public String format(int millis) { 195 return format(new Time(millis)); 196 } 197 198 /** 199 * A method that formats {@link Time}objects. 200 * 201 * @param time 202 * The time <code>Object</code> to be formatted. 203 * @return String The formatted <code>String</code> 204 */ 205 public String format(Time time) { 206 if (this.timeFormat == XSD_DURATION_FORMAT) { 207 return formatXSDDurationTime(time); 208 } 209 StringBuffer sb = new StringBuffer(); 210 sb.append(this.hourNF.format(time.getHours())); 211 sb.append(":"); 212 sb.append(minuteSecondNF.format(time.getMinutes())); 213 if (this.useSeconds) { 214 sb.append(":"); 215 sb.append(minuteSecondNF.format(time.getSeconds())); 216 } 217 if (this.useMillis) { 218 sb.append("."); 219 sb.append(milliNF.format(time.getMilliseconds())); 220 } 221 return sb.toString(); 222 } 223 224 /** 225 * Formats a date using this classe's {@link #getDateFormat() date format}. 226 * 227 * @param dateTime 228 * the date to format 229 * @param calendar 230 * the {@link java.util.Calendar Calendar} used to help format based on the Calendar's DST and other 231 * settings. 232 * @return the formatted String 233 */ 234 public String formatDateTime(Date dateTime, Calendar calendar) { 235 this.dateFormat.setCalendar(calendar); 236 if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) { 237 return getXSDateTime(dateTime, calendar); 238 } else { 239 return this.dateFormat.format(dateTime); 240 } 241 242 } 243 244 /** 245 * The date:date-time function returns the current date and time as a date/time string. The date/time string that's 246 * returned must be a string in the format defined as the lexical representation of xs:dateTime in <a 247 * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a> of <a 248 * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2: Datatypes]</a>. The date/time format is 249 * basically CCYY-MM-DDThh:mm:ss, although implementers should consult <a 250 * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2: Datatypes]</a> and <a 251 * href="http://www.iso.ch/markete/8601.pdf">[ISO 8601]</a> for details. The date/time string format must include a 252 * time zone, either a Z to indicate Coordinated Universal Time or a + or - followed by the difference between the 253 * difference from UTC represented as hh:mm. 254 */ 255 public String getXSDateTime(Date dateTime, Calendar cal) { 256 String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss"; 257 /* 258 * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) { xmlDateFormat = xsdDateTimeFormat; } 259 */ 260 SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat); 261 dateFormat.setTimeZone(getTimeZone()); 262 263 StringBuffer sb = new StringBuffer(dateFormat.format(dateTime)); 264 // Must also include offset from UTF. 265 int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);// Get the offset (in milliseconds) 266 // If there is no offset, we have "Coordinated Universal Time" 267 if (offset == 0) 268 sb.append("Z"); 269 else { 270 // Convert milliseconds to hours and minutes 271 int hrs = offset / (60 * 60 * 1000); 272 // In a few cases, the time zone may be +/-hh:30. 273 int min = offset % (60 * 60 * 1000); 274 char posneg = hrs < 0 ? '-' : '+'; 275 sb.append(posneg + formatDigits(hrs) + ':' + formatDigits(min)); 276 } 277 return sb.toString(); 278 } 279 280 /** 281 * Represent the hours and minutes with two-digit strings. 282 * 283 * @param digits 284 * hours or minutes. 285 * @return two-digit String representation of hrs or minutes. 286 */ 287 private static String formatDigits(int digits) { 288 String dd = String.valueOf(Math.abs(digits)); 289 return dd.length() == 1 ? '0' + dd : dd; 290 } 291 292 /** 293 * This returns the xml representation of an xsd:duration object. 294 * 295 * @param millis 296 * the duration in milliseconds 297 * @return the xsd:duration formatted String 298 */ 299 public String formatXSDDurationTime(long millis) { 300 return formatXSDDurationTime(new Time(millis)); 301 } 302 303 /** 304 * This returns the xml representation of an xsd:duration object. 305 * 306 * @param time 307 * the duration as a Time object 308 * @return the xsd:duration formatted String 309 */ 310 public String formatXSDDurationTime(Time time) { 311 StringBuffer duration = new StringBuffer(); 312 if (time.getHours() != 0 || time.getMinutes() != 0 || time.getSeconds() != 0 || time.getMilliseconds() != 0) { 313 duration.append("P"); 314 duration.append("T"); 315 316 if (time.getHours() != 0) 317 duration.append(time.getHours() + "H"); 318 319 if (time.getMinutes() != 0) 320 duration.append(time.getMinutes() + "M"); 321 322 if (time.getSeconds() != 0 || time.getMilliseconds() != 0) { 323 duration.append(time.getSeconds() + "." + milliNF.format(time.getMilliseconds())); 324 duration.append("S"); 325 } 326 if (duration.length() == 1) // zero seconds 327 duration.append("T0S"); 328 if (time.isNegative()) 329 duration.insert(0, "-"); 330 } 331 return duration.toString(); 332 } 333 334 /** 335 * A method that returns an XML formatted <code>String</code> representing the serialized <code>Object</code>. The 336 * format used is: 337 * 338 * <pre> 339 * <AstronomicalTimes date="1969-02-08" type="net.sourceforge.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"> 340 * <Sunrise>2007-02-18T06:45:27-05:00</Sunrise> 341 * <TemporalHour>PT54M17.529S</TemporalHour> 342 * ... 343 * </AstronomicalTimes> 344 * </pre> 345 * 346 * Note that the output uses the <a href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a> format for 347 * times such as sunrise, and <a href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a> format for 348 * times that are a duration such as the length of a 349 * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is 350 * returned by the {@link #toString() toString} . 351 * 352 * @return The XML formatted <code>String</code>. The format will be: 353 * 354 * <pre> 355 * <AstronomicalTimes date="1969-02-08" type="net.sourceforge.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"> 356 * <Sunrise>2007-02-18T06:45:27-05:00</Sunrise> 357 * <TemporalHour>PT54M17.529S</TemporalHour> 358 * ... 359 * </AstronomicalTimes> 360 * </pre> 361 * 362 * TODO: add proper schema, and support for nulls. XSD duration (for solar hours), should probably return 363 * nil and not P 364 */ 365 public static String toXML(AstronomicalCalendar ac) { 366 ZmanimFormatter formatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat( 367 "yyyy-MM-dd'T'HH:mm:ss"), ac.getGeoLocation().getTimeZone()); 368 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 369 df.setTimeZone(ac.getGeoLocation().getTimeZone()); 370 371 StringBuffer sb = new StringBuffer("<"); 372 if (ac.getClass().getName().equals("net.sourceforge.zmanim.AstronomicalCalendar")) { 373 sb.append("AstronomicalTimes"); 374 // TODO: use proper schema ref, and maybe build a real schema. 375 // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "; 376 // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim astronomical.xsd" 377 } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ComplexZmanimCalendar")) { 378 sb.append("Zmanim"); 379 // TODO: use proper schema ref, and maybe build a real schema. 380 // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "; 381 // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim zmanim.xsd" 382 } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ZmanimCalendar")) { 383 sb.append("BasicZmanim"); 384 // TODO: use proper schema ref, and maybe build a real schema. 385 // output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "; 386 // output += xsi:schemaLocation="http://www.kosherjava.com/zmanim basicZmanim.xsd" 387 } 388 sb.append(" date=\"").append(df.format(ac.getCalendar().getTime())).append("\""); 389 sb.append(" type=\"").append(ac.getClass().getName()).append("\""); 390 sb.append(" algorithm=\"").append(ac.getAstronomicalCalculator().getCalculatorName()).append("\""); 391 sb.append(" location=\"").append(ac.getGeoLocation().getLocationName()).append("\""); 392 sb.append(" latitude=\"").append(ac.getGeoLocation().getLatitude()).append("\""); 393 sb.append(" longitude=\"").append(ac.getGeoLocation().getLongitude()).append("\""); 394 sb.append(" elevation=\"").append(ac.getGeoLocation().getElevation()).append("\""); 395 sb.append(" timeZoneName=\"").append(ac.getGeoLocation().getTimeZone().getDisplayName()).append("\""); 396 sb.append(" timeZoneID=\"").append(ac.getGeoLocation().getTimeZone().getID()).append("\""); 397 sb.append(" timeZoneOffset=\"") 398 .append((ac.getGeoLocation().getTimeZone().getOffset(ac.getCalendar().getTimeInMillis()) / ((double) HOUR_MILLIS))) 399 .append("\""); 400 401 sb.append(">\n"); 402 403 Method[] theMethods = ac.getClass().getMethods(); 404 String tagName = ""; 405 Object value = null; 406 List<Zman> dateList = new ArrayList<Zman>(); 407 List<Zman> durationList = new ArrayList<Zman>(); 408 List<String> otherList = new ArrayList<String>(); 409 for (int i = 0; i < theMethods.length; i++) { 410 if (includeMethod(theMethods[i])) { 411 tagName = theMethods[i].getName().substring(3); 412 // String returnType = theMethods[i].getReturnType().getName(); 413 try { 414 value = theMethods[i].invoke(ac, (Object[]) null); 415 if (value == null) {// TODO: Consider using reflection to determine the return type, not the value 416 otherList.add("<" + tagName + ">N/A</" + tagName + ">"); 417 // TODO: instead of N/A, consider return proper xs:nil. 418 // otherList.add("<" + tagName + " xs:nil=\"true\" />"); 419 } else if (value instanceof Date) { 420 dateList.add(new Zman((Date) value, tagName)); 421 } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis 422 if (((Long) value).longValue() == Long.MIN_VALUE) { 423 otherList.add("<" + tagName + ">N/A</" + tagName + ">"); 424 // TODO: instead of N/A, consider return proper xs:nil. 425 // otherList.add("<" + tagName + " xs:nil=\"true\" />"); 426 } else { 427 durationList.add(new Zman((int) ((Long) value).longValue(), tagName)); 428 } 429 } else { // will probably never enter this block, but is present to be future proof 430 otherList.add("<" + tagName + ">" + value + "</" + tagName + ">"); 431 } 432 } catch (Exception e) { 433 e.printStackTrace(); 434 } 435 } 436 } 437 Zman zman; 438 Collections.sort(dateList, Zman.DATE_ORDER); 439 440 for (int i = 0; i < dateList.size(); i++) { 441 zman = (Zman) dateList.get(i); 442 sb.append("\t<").append(zman.getZmanLabel()).append(">"); 443 sb.append(formatter.formatDateTime(zman.getZman(), ac.getCalendar())); 444 sb.append("</").append(zman.getZmanLabel()).append(">\n"); 445 } 446 Collections.sort(durationList, Zman.DURATION_ORDER); 447 for (int i = 0; i < durationList.size(); i++) { 448 zman = (Zman) durationList.get(i); 449 sb.append("\t<" + zman.getZmanLabel()).append(">"); 450 sb.append(formatter.format((int) zman.getDuration())).append("</").append(zman.getZmanLabel()) 451 .append(">\n"); 452 } 453 454 for (int i = 0; i < otherList.size(); i++) {// will probably never enter this block 455 sb.append("\t").append(otherList.get(i)).append("\n"); 456 } 457 458 if (ac.getClass().getName().equals("net.sourceforge.zmanim.AstronomicalCalendar")) { 459 sb.append("</AstronomicalTimes>"); 460 } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ComplexZmanimCalendar")) { 461 sb.append("</Zmanim>"); 462 } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ZmanimCalendar")) { 463 sb.append("</Basic>"); 464 } 465 return sb.toString(); 466 } 467 468 /** 469 * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)} 470 * 471 * @param method 472 * @return 473 */ 474 private static boolean includeMethod(Method method) { 475 List<String> methodWhiteList = new ArrayList<String>(); 476 // methodWhiteList.add("getName"); 477 478 List<String> methodBlackList = new ArrayList<String>(); 479 // methodBlackList.add("getGregorianChange"); 480 481 if (methodWhiteList.contains(method.getName())) 482 return true; 483 if (methodBlackList.contains(method.getName())) 484 return false; 485 486 if (method.getParameterTypes().length > 0) 487 return false; // Skip get methods with parameters since we do not know what value to pass 488 if (!method.getName().startsWith("get")) 489 return false; 490 491 if (method.getReturnType().getName().endsWith("Date") || method.getReturnType().getName().endsWith("long")) { 492 return true; 493 } 494 return false; 495 } 496}