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