1 /* ============================================================
2  * JRobin : Pure java implementation of RRDTool's functionality
3  * ============================================================
4  *
5  * Project Info:  http://www.jrobin.org
6  * Project Lead:  Sasa Markovic (saxon@jrobin.org);
7  *
8  * (C) Copyright 2003-2005, by Sasa Markovic.
9  *
10  * Developers:    Sasa Markovic (saxon@jrobin.org)
11  *
12  *
13  * This library is free software; you can redistribute it and/or modify it under the terms
14  * of the GNU Lesser General Public License as published by the Free Software Foundation;
15  * either version 2.1 of the License, or (at your option) any later version.
16  *
17  * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
18  * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19  * See the GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License along with this
22  * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
23  * Boston, MA 02111-1307, USA.
24  */

25
26 package org.jrobin.core;
27
28 import org.jrobin.core.timespec.TimeParser;
29 import org.jrobin.core.timespec.TimeSpec;
30 import org.w3c.dom.Document;
31 import org.w3c.dom.Element;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34 import org.xml.sax.InputSource;
35 import org.xml.sax.SAXException;
36
37 import javax.xml.parsers.DocumentBuilder;
38 import javax.xml.parsers.DocumentBuilderFactory;
39 import javax.xml.parsers.ParserConfigurationException;
40 import java.awt.*;
41 import java.io.*;
42 import java.text.DecimalFormat;
43 import java.text.NumberFormat;
44 import java.text.ParseException;
45 import java.text.SimpleDateFormat;
46 import java.util.ArrayList;
47 import java.util.Calendar;
48 import java.util.Date;
49 import java.util.Locale;
50
51 /**
52  * Class defines various utility functions used in JRobin.
53  *
54  * @author <a href="mailto:saxon@jrobin.org">Sasa Markovic</a>
55  */

56 public class Util {
57
58     public static final long MAX_LONG = Long.MAX_VALUE;
59     public static final long MIN_LONG = -Long.MAX_VALUE;
60
61     public static final double MAX_DOUBLE = Double.MAX_VALUE;
62     public static final double MIN_DOUBLE = -Double.MAX_VALUE;
63
64     // pattern RRDTool uses to format doubles in XML files
65     static final String PATTERN = "0.0000000000E00";
66     // directory under $USER_HOME used for demo graphs storing
67     static final String JROBIN_DIR = "jrobin-demo";
68
69     static final DecimalFormat df;
70
71     static {
72         df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.ENGLISH);
73         df.applyPattern(PATTERN);
74         df.setPositivePrefix("+");
75     }
76
77     /**
78      * Converts an array of long primitives to an array of doubles.
79      *
80      * @return Same array but with all values as double.
81      */

82     public static double[] toDoubleArray(final long[] array) {
83         double[] values = new double[ array.length ];
84         for (int i = 0; i < array.length; i++) {
85             values[i] = array[i];
86         }
87         return values;
88     }
89
90     /**
91      * Returns current timestamp in seconds (without milliseconds). Returned timestamp
92      * is obtained with the following expression: <p>
93      * <p/>
94      * <code>(System.currentTimeMillis() + 500L) / 1000L</code>
95      *
96      * @return Current timestamp
97      */

98     public static long getTime() {
99         return (System.currentTimeMillis() + 500L) / 1000L;
100     }
101
102     /**
103      * Just an alias for {@link #getTime()} method.
104      *
105      * @return Current timestamp (without milliseconds)
106      */

107     public static long getTimestamp() {
108         return getTime();
109     }
110
111     /**
112      * Rounds the given timestamp to the nearest whole &quote;step&quote;. Rounded value is obtained
113      * from the following expression:<p>
114      * <code>timestamp - timestamp % step;</code><p>
115      *
116      * @param timestamp Timestamp in seconds
117      * @param step      Step in seconds
118      * @return "Rounded" timestamp
119      */

120     public static long normalize(long timestamp, long step) {
121         return timestamp - timestamp % step;
122     }
123
124     /**
125      * Returns the greater of two double values, but treats NaN as the smallest possible
126      * value. Note that <code>Math.max()</code> behaves differently for NaN arguments.
127      *
128      * @param x an argument
129      * @param y another argument
130      * @return the lager of arguments
131      */

132     public static double max(double x, double y) {
133         return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.max(x, y);
134     }
135
136     /**
137      * Returns the smaller of two double values, but treats NaN as the greatest possible
138      * value. Note that <code>Math.min()</code> behaves differently for NaN arguments.
139      *
140      * @param x an argument
141      * @param y another argument
142      * @return the smaller of arguments
143      */

144     public static double min(double x, double y) {
145         return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.min(x, y);
146     }
147
148     /**
149      * Calculates sum of two doubles, but treats NaNs as zeros.
150      *
151      * @param x First double
152      * @param y Second double
153      * @return Sum(x,y) calculated as <code>Double.isNaN(x)? y: Double.isNaN(y)? x: x + y;</code>
154      */

155     public static double sum(double x, double y) {
156         return Double.isNaN(x) ? y : Double.isNaN(y) ? x : x + y;
157     }
158
159     static String formatDouble(double x, String nanString, boolean forceExponents) {
160         if (Double.isNaN(x)) {
161             return nanString;
162         }
163         if (forceExponents) {
164             return df.format(x);
165         }
166         return "" + x;
167     }
168
169     static String formatDouble(double x, boolean forceExponents) {
170         return formatDouble(x, "" + Double.NaN, forceExponents);
171     }
172
173     /**
174      * Formats double as a string using exponential notation (RRDTool like). Used for debugging
175      * throught the project.
176      *
177      * @param x value to be formatted
178      * @return string like "+1.234567E+02"
179      */

180     public static String formatDouble(double x) {
181         return formatDouble(x, true);
182     }
183
184     /**
185      * Returns <code>Date</code> object for the given timestamp (in seconds, without
186      * milliseconds)
187      *
188      * @param timestamp Timestamp in seconds.
189      * @return Corresponding Date object.
190      */

191     public static Date getDate(long timestamp) {
192         return new Date(timestamp * 1000L);
193     }
194
195     /**
196      * Returns <code>Calendar</code> object for the given timestamp
197      * (in seconds, without milliseconds)
198      *
199      * @param timestamp Timestamp in seconds.
200      * @return Corresponding Calendar object.
201      */

202     public static Calendar getCalendar(long timestamp) {
203         Calendar calendar = Calendar.getInstance();
204         calendar.setTimeInMillis(timestamp * 1000L);
205         return calendar;
206     }
207
208     /**
209      * Returns <code>Calendar</code> object for the given Date object
210      *
211      * @param date Date object
212      * @return Corresponding Calendar object.
213      */

214     public static Calendar getCalendar(Date date) {
215         Calendar calendar = Calendar.getInstance();
216         calendar.setTime(date);
217         return calendar;
218     }
219
220     /**
221      * Returns timestamp (unix epoch) for the given Date object
222      *
223      * @param date Date object
224      * @return Corresponding timestamp (without milliseconds)
225      */

226     public static long getTimestamp(Date date) {
227         // round to whole seconds, ignore milliseconds
228         return (date.getTime() + 499L) / 1000L;
229     }
230
231     /**
232      * Returns timestamp (unix epoch) for the given Calendar object
233      *
234      * @param gc Calendar object
235      * @return Corresponding timestamp (without milliseconds)
236      */

237     public static long getTimestamp(Calendar gc) {
238         return getTimestamp(gc.getTime());
239     }
240
241     /**
242      * Returns timestamp (unix epoch) for the given year, month, day, hour and minute.
243      *
244      * @param year  Year
245      * @param month Month (zero-based)
246      * @param day   Day in month
247      * @param hour  Hour
248      * @param min   Minute
249      * @return Corresponding timestamp
250      */

251     public static long getTimestamp(int year, int month, int day, int hour, int min) {
252         Calendar calendar = Calendar.getInstance();
253         calendar.clear();
254         calendar.set(year, month, day, hour, min);
255         return Util.getTimestamp(calendar);
256     }
257
258     /**
259      * Returns timestamp (unix epoch) for the given year, month and day.
260      *
261      * @param year  Year
262      * @param month Month (zero-based)
263      * @param day   Day in month
264      * @return Corresponding timestamp
265      */

266     public static long getTimestamp(int year, int month, int day) {
267         return Util.getTimestamp(year, month, day, 0, 0);
268     }
269
270     /**
271      * Parses at-style time specification and returns the corresponding timestamp. For example:<p>
272      * <pre>
273      * long t = Util.getTimestamp("now-1d");
274      * </pre>
275      *
276      * @param atStyleTimeSpec at-style time specification. For the complete explanation of the syntax
277      *                        allowed see RRDTool's <code>rrdfetch</code> man page.<p>
278      * @return timestamp in seconds since epoch.
279      * @throws RrdException Thrown if invalid time specification is supplied.
280      */

281     public static long getTimestamp(String atStyleTimeSpec) throws RrdException {
282         TimeSpec timeSpec = new TimeParser(atStyleTimeSpec).parse();
283         return timeSpec.getTimestamp();
284     }
285
286     /**
287      * Parses two related at-style time specifications and returns corresponding timestamps. For example:<p>
288      * <pre>
289      * long[] t = Util.getTimestamps("end-1d","now");
290      * </pre>
291      *
292      * @param atStyleTimeSpec1 Starting at-style time specification. For the complete explanation of the syntax
293      *                         allowed see RRDTool's <code>rrdfetch</code> man page.<p>
294      * @param atStyleTimeSpec2 Ending at-style time specification. For the complete explanation of the syntax
295      *                         allowed see RRDTool's <code>rrdfetch</code> man page.<p>
296      * @return An array of two longs representing starting and ending timestamp in seconds since epoch.
297      * @throws RrdException Thrown if any input time specification is invalid.
298      */

299     public static long[] getTimestamps(String atStyleTimeSpec1, String atStyleTimeSpec2) throws RrdException {
300         TimeSpec timeSpec1 = new TimeParser(atStyleTimeSpec1).parse();
301         TimeSpec timeSpec2 = new TimeParser(atStyleTimeSpec2).parse();
302         return TimeSpec.getTimestamps(timeSpec1, timeSpec2);
303     }
304
305     /**
306      * Parses input string as a double value. If the value cannot be parsed, Double.NaN
307      * is returned (NumberFormatException is never thrown).
308      *
309      * @param valueStr String representing double value
310      * @return a double corresponding to the input string
311      */

312     public static double parseDouble(String valueStr) {
313         double value;
314         try {
315             value = Double.parseDouble(valueStr);
316         }
317         catch (NumberFormatException nfe) {
318             value = Double.NaN;
319         }
320         return value;
321     }
322
323     /**
324      * Checks if a string can be parsed as double.
325      *
326      * @param s Input string
327      * @return <code>true</code> if the string can be parsed as double, <code>false</code> otherwise
328      */

329     public static boolean isDouble(String s) {
330         try {
331             Double.parseDouble(s);
332             return true;
333         }
334         catch (NumberFormatException nfe) {
335             return false;
336         }
337     }
338
339     /**
340      * Parses input string as a boolean value. The parser is case insensitive.
341      *
342      * @param valueStr String representing boolean value
343      * @return <code>true</code>, if valueStr equals to 'true', 'on', 'yes', 'y' or '1';
344      *         <code>false</code> in all other cases.
345      */

346     public static boolean parseBoolean(String valueStr) {
347         return valueStr.equalsIgnoreCase("true") ||
348                 valueStr.equalsIgnoreCase("on") ||
349                 valueStr.equalsIgnoreCase("yes") ||
350                 valueStr.equalsIgnoreCase("y") ||
351                 valueStr.equalsIgnoreCase("1");
352     }
353
354     /**
355      * Parses input string as color. The color string should be of the form #RRGGBB (no alpha specified,
356      * opaque color) or #RRGGBBAA (alpa specified, transparent colors). Leading character '#' is
357      * optional.
358      *
359      * @param valueStr Input string, for example #FFAA24, #AABBCC33, 010203 or ABC13E4F
360      * @return Paint object
361      * @throws RrdException If the input string is not 6 or 8 characters long (without optional '#')
362      */

363     public static Paint parseColor(String valueStr) throws RrdException {
364         String c = valueStr.startsWith("#") ? valueStr.substring(1) : valueStr;
365         if (c.length() != 6 && c.length() != 8) {
366             throw new RrdException("Invalid color specification: " + valueStr);
367         }
368         String r = c.substring(0, 2), g = c.substring(2, 4), b = c.substring(4, 6);
369         if (c.length() == 6) {
370             return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16), Integer.parseInt(b, 16));
371         }
372         else {
373             String a = c.substring(6);
374             return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16),
375                     Integer.parseInt(b, 16), Integer.parseInt(a, 16));
376         }
377     }
378
379     /**
380      * Returns file system separator string.
381      *
382      * @return File system separator ("/" on Unix, "\" on Windows)
383      */

384     public static String getFileSeparator() {
385         return System.getProperty("file.separator");
386     }
387
388     /**
389      * Returns path to user's home directory.
390      *
391      * @return Path to users home directory, with file separator appended.
392      */

393     public static String getUserHomeDirectory() {
394         return System.getProperty("user.home") + getFileSeparator();
395     }
396
397     /**
398      * Returns path to directory used for placement of JRobin demo graphs and creates it
399      * if necessary.
400      *
401      * @return Path to demo directory (defaults to $HOME/jrobin/) if directory exists or
402      *         was successfully created. Null if such directory could not be created.
403      */

404     public static String getJRobinDemoDirectory() {
405         String homeDirPath = getUserHomeDirectory() + JROBIN_DIR + getFileSeparator();
406         File homeDirFile = new File(homeDirPath);
407         return (homeDirFile.exists() || homeDirFile.mkdirs()) ? homeDirPath : null;
408     }
409
410     /**
411      * Returns full path to the file stored in the demo directory of JRobin
412      *
413      * @param filename Partial path to the file stored in the demo directory of JRobin
414      *                 (just name and extension, without parent directories)
415      * @return Full path to the file
416      */

417     public static String getJRobinDemoPath(String filename) {
418         String demoDir = getJRobinDemoDirectory();
419         if (demoDir != null) {
420             return demoDir + filename;
421         }
422         else {
423             return null;
424         }
425     }
426
427     static boolean sameFilePath(String path1, String path2) throws IOException {
428         File file1 = new File(path1);
429         File file2 = new File(path2);
430         return file1.getCanonicalPath().equals(file2.getCanonicalPath());
431     }
432
433     static int getMatchingDatasourceIndex(RrdDb rrd1, int dsIndex, RrdDb rrd2) throws IOException {
434         String dsName = rrd1.getDatasource(dsIndex).getDsName();
435         try {
436             return rrd2.getDsIndex(dsName);
437         }
438         catch (RrdException e) {
439             return -1;
440         }
441     }
442
443     static int getMatchingArchiveIndex(RrdDb rrd1, int arcIndex, RrdDb rrd2)
444             throws IOException {
445         Archive archive = rrd1.getArchive(arcIndex);
446         String consolFun = archive.getConsolFun();
447         int steps = archive.getSteps();
448         try {
449             return rrd2.getArcIndex(consolFun, steps);
450         }
451         catch (RrdException e) {
452             return -1;
453         }
454     }
455
456     static String getTmpFilename() throws IOException {
457         return File.createTempFile("JROBIN_"".tmp").getCanonicalPath();
458     }
459
460     static final String ISO_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";   // ISO
461
462     /**
463      * Creates Calendar object from a string. The string should represent
464      * either a long integer (UNIX timestamp in seconds without milliseconds,
465      * like "1002354657") or a human readable date string in the format "yyyy-MM-dd HH:mm:ss"
466      * (like "2004-02-25 12:23:45").
467      *
468      * @param timeStr Input string
469      * @return Calendar object
470      */

471     public static Calendar getCalendar(String timeStr) {
472         // try to parse it as long
473         try {
474             long timestamp = Long.parseLong(timeStr);
475             return Util.getCalendar(timestamp);
476         }
477         catch (NumberFormatException nfe) {
478             // not a long timestamp, try to parse it as data
479             SimpleDateFormat df = new SimpleDateFormat(ISO_DATE_FORMAT);
480             df.setLenient(false);
481             try {
482                 Date date = df.parse(timeStr);
483                 return Util.getCalendar(date);
484             }
485             catch (ParseException pe) {
486                 throw new IllegalArgumentException("Time/date not in " + ISO_DATE_FORMAT +
487                         " format: " + timeStr);
488             }
489         }
490     }
491
492     /**
493      * Various DOM utility functions
494      */

495     public static class Xml {
496         public static Node[] getChildNodes(Node parentNode) {
497             return getChildNodes(parentNode, null);
498         }
499
500         public static Node[] getChildNodes(Node parentNode, String childName) {
501             ArrayList<Node> nodes = new ArrayList<Node>();
502             NodeList nodeList = parentNode.getChildNodes();
503             for (int i = 0; i < nodeList.getLength(); i++) {
504                 Node node = nodeList.item(i);
505                 if (childName == null || node.getNodeName().equals(childName)) {
506                     nodes.add(node);
507                 }
508             }
509             return nodes.toArray(new Node[0]);
510         }
511
512         public static Node getFirstChildNode(Node parentNode, String childName) throws RrdException {
513             Node[] childs = getChildNodes(parentNode, childName);
514             if (childs.length > 0) {
515                 return childs[0];
516             }
517             throw new RrdException("XML Error, no such child: " + childName);
518         }
519
520         public static boolean hasChildNode(Node parentNode, String childName) {
521             Node[] childs = getChildNodes(parentNode, childName);
522             return childs.length > 0;
523         }
524
525         // -- Wrapper around getChildValue with trim
526         public static String getChildValue(Node parentNode, String childName) throws RrdException {
527             return getChildValue(parentNode, childName, true);
528         }
529
530         public static String getChildValue(Node parentNode, String childName, boolean trim) throws RrdException {
531             NodeList children = parentNode.getChildNodes();
532             for (int i = 0; i < children.getLength(); i++) {
533                 Node child = children.item(i);
534                 if (child.getNodeName().equals(childName)) {
535                     return getValue(child, trim);
536                 }
537             }
538             throw new RrdException("XML Error, no such child: " + childName);
539         }
540
541         // -- Wrapper around getValue with trim
542         public static String getValue(Node node) {
543             return getValue(node, true);
544         }
545
546         public static String getValue(Node node, boolean trimValue) {
547             String value = null;
548             Node child = node.getFirstChild();
549             if (child != null) {
550                 value = child.getNodeValue();
551                 if (value != null && trimValue) {
552                     value = value.trim();
553                 }
554             }
555             return value;
556         }
557
558         public static int getChildValueAsInt(Node parentNode, String childName) throws RrdException {
559             String valueStr = getChildValue(parentNode, childName);
560             return Integer.parseInt(valueStr);
561         }
562
563         public static int getValueAsInt(Node node) {
564             String valueStr = getValue(node);
565             return Integer.parseInt(valueStr);
566         }
567
568         public static long getChildValueAsLong(Node parentNode, String childName) throws RrdException {
569             String valueStr = getChildValue(parentNode, childName);
570             return Long.parseLong(valueStr);
571         }
572
573         public static long getValueAsLong(Node node) {
574             String valueStr = getValue(node);
575             return Long.parseLong(valueStr);
576         }
577
578         public static double getChildValueAsDouble(Node parentNode, String childName) throws RrdException {
579             String valueStr = getChildValue(parentNode, childName);
580             return Util.parseDouble(valueStr);
581         }
582
583         public static double getValueAsDouble(Node node) {
584             String valueStr = getValue(node);
585             return Util.parseDouble(valueStr);
586         }
587
588         public static boolean getChildValueAsBoolean(Node parentNode, String childName) throws RrdException {
589             String valueStr = getChildValue(parentNode, childName);
590             return Util.parseBoolean(valueStr);
591         }
592
593         public static boolean getValueAsBoolean(Node node) {
594             String valueStr = getValue(node);
595             return Util.parseBoolean(valueStr);
596         }
597
598         public static Element getRootElement(InputSource inputSource) throws RrdException, IOException {
599             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
600             factory.setValidating(false);
601             factory.setNamespaceAware(false);
602             try {
603                 DocumentBuilder builder = factory.newDocumentBuilder();
604                 Document doc = builder.parse(inputSource);
605                 return doc.getDocumentElement();
606             }
607             catch (ParserConfigurationException e) {
608                 throw new RrdException(e);
609             }
610             catch (SAXException e) {
611                 throw new RrdException(e);
612             }
613         }
614
615         public static Element getRootElement(String xmlString) throws RrdException, IOException {
616             return getRootElement(new InputSource(new StringReader(xmlString)));
617         }
618
619         public static Element getRootElement(File xmlFile) throws RrdException, IOException {
620             Reader reader = null;
621             try {
622                 reader = new FileReader(xmlFile);
623                 return getRootElement(new InputSource(reader));
624             }
625             finally {
626                 if (reader != null) {
627                     reader.close();
628                 }
629             }
630         }
631     }
632
633     private static long lastLap = System.currentTimeMillis();
634
635     /**
636      * Function used for debugging purposes and performance bottlenecks detection.
637      * Probably of no use for end users of JRobin.
638      *
639      * @return String representing time in seconds since last
640      *         <code>getLapTime()</code> method call.
641      */

642     public static String getLapTime() {
643         long newLap = System.currentTimeMillis();
644         double seconds = (newLap - lastLap) / 1000.0;
645         lastLap = newLap;
646         return "[" + seconds + " sec]";
647     }
648
649     /**
650      * Returns the root directory of the JRobin distribution. Useful in some demo applications,
651      * probably of no use anywhere else.<p>
652      * <p/>
653      * The function assumes that all JRobin .class files are placed under
654      * the &lt;root&gt;/classes subdirectory and that all jars (libraries) are placed in the
655      * &lt;root&gt;/lib subdirectory (the original JRobin directory structure).<p>
656      *
657      * @return absolute path to JRobin's home directory
658      */

659     public static String getJRobinHomeDirectory() {
660         String className = Util.class.getName().replace('.', '/');
661         String uri = Util.class.getResource("/" + className + ".class").toString();
662         //System.out.println(uri);
663         if (uri.startsWith("file:/")) {
664             uri = uri.substring(6);
665             File file = new File(uri);
666             // let's go 5 steps backwards
667             for (int i = 0; i < 5; i++) {
668                 file = file.getParentFile();
669             }
670             uri = file.getAbsolutePath();
671         }
672         else if (uri.startsWith("jar:file:/")) {
673             uri = uri.substring(9, uri.lastIndexOf('!'));
674             File file = new File(uri);
675             // let's go 2 steps backwards
676             for (int i = 0; i < 2; i++) {
677                 file = file.getParentFile();
678             }
679             uri = file.getAbsolutePath();
680         }
681         else {
682             uri = null;
683         }
684         return uri;
685     }
686
687     /**
688      * Compares two doubles but treats all NaNs as equal.
689      * In Java (by default) Double.NaN == Double.NaN always returns <code>false</code>
690      *
691      * @param x the first value
692      * @param y the second value
693      * @return <code>true</code> if x and y are both equal to Double.NaN, or if x == y. <code>false</code> otherwise
694      */

695     public static boolean equal(double x, double y) {
696         return (Double.isNaN(x) && Double.isNaN(y)) || (x == y);
697     }
698
699     /**
700      * Returns canonical file path for the given file path
701      *
702      * @param path Absolute or relative file path
703      * @return Canonical file path
704      * @throws IOException Thrown if canonical file path could not be resolved
705      */

706     public static String getCanonicalPath(String path) throws IOException {
707         return new File(path).getCanonicalPath();
708     }
709
710     /**
711      * Returns last modification time for the given file.
712      *
713      * @param file File object representing file on the disk
714      * @return Last modification time in seconds (without milliseconds)
715      */

716     public static long getLastModified(String file) {
717         return (new File(file).lastModified() + 500L) / 1000L;
718     }
719
720     /**
721      * Checks if the file with the given file name exists
722      *
723      * @param filename File name
724      * @return <code>true</code> if file exists, <code>false</code> otherwise
725      */

726     public static boolean fileExists(String filename) {
727         return new File(filename).exists();
728     }
729
730     /**
731      * Finds max value for an array of doubles (NaNs are ignored). If all values in the array
732      * are NaNs, NaN is returned.
733      *
734      * @param values Array of double values
735      * @return max value in the array (NaNs are ignored)
736      */

737     public static double max(double[] values) {
738         double max = Double.NaN;
739         for (double value : values) {
740             max = Util.max(max, value);
741         }
742         return max;
743     }
744
745     /**
746      * Finds min value for an array of doubles (NaNs are ignored). If all values in the array
747      * are NaNs, NaN is returned.
748      *
749      * @param values Array of double values
750      * @return min value in the array (NaNs are ignored)
751      */

752     public static double min(double[] values) {
753         double min = Double.NaN;
754         for (double value : values) {
755             min = Util.min(min, value);
756         }
757         return min;
758     }
759
760     /**
761      * Equivalent of the C-style sprintf function. Sorry, it works only in Java5.
762      *
763      * @param format Format string
764      * @param args   Arbitrary list of arguments
765      * @return Formatted string
766      */

767     public static String sprintf(String format, Object ... args) {
768         String fmt = format.replaceAll("([^%]|^)%([^a-zA-Z%]*)l(f|g|e)""$1%$2$3");
769         return String.format(fmt, args);
770     }
771 }