001package jmri.util;
002
003import java.text.ParseException;
004import java.util.*;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007
008import javax.annotation.Nonnull;
009import javax.annotation.CheckForNull;
010
011import jmri.Reportable;
012
013
014/**
015 * Converts between java types, for example String to Double and double to boolean.
016 *
017 * @author Daniel Bergqvist Copyright 2019
018 */
019public final class TypeConversionUtil {
020
021    /**
022     * Is this object an integer number?
023     * <P>
024     * The method returns true if the object is any of these classes:
025     * <ul>
026     *   <li>AtomicInteger</li>
027     *   <li>AtomicLong</li>
028     *   <li>BigInteger</li>
029     *   <li>Byte</li>
030     *   <li>Short</li>
031     *   <li>Integer</li>
032     *   <li>Long</li>
033     * </ul>
034     * @param object the object to check
035     * @return true if the object is an object that is an integer, false otherwise
036     */
037    public static boolean isIntegerNumber(Object object) {
038        return (object instanceof java.util.concurrent.atomic.AtomicInteger)
039                || (object instanceof java.util.concurrent.atomic.AtomicLong)
040                || (object instanceof java.math.BigInteger)
041                || (object instanceof Byte)
042                || (object instanceof Short)
043                || (object instanceof Integer)
044                || (object instanceof Long);
045    }
046
047    /**
048     * Is this object an integer or a floating point number?
049     * <P>
050     * The method returns true if the object is any of these classes:
051     * <ul>
052     *   <li>AtomicInteger</li>
053     *   <li>AtomicLong</li>
054     *   <li>BigInteger</li>
055     *   <li>Byte</li>
056     *   <li>Short</li>
057     *   <li>Integer</li>
058     *   <li>Long</li>
059     *   <li>BigDecimal</li>
060     *   <li>Float</li>
061     *   <li>Double</li>
062     * </ul>
063     * @param object the object to check
064     * @return true if the object is an object that is either an integer or a
065     * float, false otherwise
066     */
067    public static boolean isFloatingNumber(Object object) {
068        return isIntegerNumber(object)
069                || (object instanceof java.math.BigDecimal)
070                || (object instanceof Float)
071                || (object instanceof Double);
072    }
073
074    /**
075     * Is this object a String?
076     * @param object the object to check
077     * @return true if the object is a String, false otherwise
078     */
079    public static boolean isString(Object object) {
080        return object instanceof String;
081    }
082
083
084    /**
085     * Convert a value to a boolean.
086     * <P>
087     * Rules:
088     * "0" string is converted to false
089     * "0.000" string is converted to false, if the number of decimals is &gt; 0
090     * An integer number is converted to false if the number is 0
091     * A floating number is converted to false if the number is -0.5 &lt; x &lt; 0.5
092     * The string "true" (case insensitive) returns true.
093     * The string "false" (case insensitive) returns false.
094     * A Reportable is first converted to a string using toReportString() and then
095     * treated as a string.
096     * A JSON TextNode is first converted to a string using asText() and then
097     * treated as a string.
098     * Everything else throws an exception.
099     * <P>
100     * For objects that implement the Reportable interface, the value is fetched
101     * from the method toReportString().
102     *
103     * @param value the value to convert
104     * @param do_i18n true if internationalization should be done, false otherwise
105     * @return the boolean value
106     */
107    public static boolean convertToBoolean(@CheckForNull Object value, boolean do_i18n) {
108        if (value instanceof Boolean) {
109            return ((Boolean)value);
110        }
111
112        // JSON text node
113        if (value instanceof com.fasterxml.jackson.databind.node.TextNode) {
114            value = ((com.fasterxml.jackson.databind.node.TextNode)value).asText();
115        }
116
117        if (value instanceof Reportable) {
118            value = ((Reportable)value).toReportString();
119        }
120
121        if (value == null) {
122            throw new IllegalArgumentException("Value is null");
123        }
124
125        try {
126            // Can the value be converted to a long?
127            value = convertToLong(value, true, true, false);
128        } catch (NumberFormatException e) {
129            try {
130                // Can the value be converted to a double?
131                value = convertToDouble(value, do_i18n, true, true, false);
132            } catch (NumberFormatException e2) {
133                // Do nothing
134            }
135        }
136
137        if (value instanceof Number) {
138            double number = ((Number)value).doubleValue();
139            return ! ((-0.5 < number) && (number < 0.5));
140        } else if (value instanceof Boolean) {
141            return (Boolean)value;
142        } else {
143            switch (value.toString().toLowerCase()) {
144                case "false": return false;
145                case "true": return true;
146                default: throw new IllegalArgumentException(String.format("Value \"%s\" can't be converted to a boolean", value));
147            }
148        }
149    }
150
151    private static boolean convertStringToBoolean_JythonRules(@Nonnull String str, boolean do_i18n) {
152        // try to parse the string as a number
153        try {
154            double number;
155            if (do_i18n) {
156                number = IntlUtilities.doubleValue(str);
157            } else {
158                number = Double.parseDouble(str);
159            }
160//                System.err.format("The string: '%s', result: %1.4f%n", str, (float)number);
161            return ! ((-0.5 < number) && (number < 0.5));
162        } catch (NumberFormatException | ParseException ex) {
163            log.debug("The string '{}' cannot be parsed as a number", str);
164        }
165
166//            System.err.format("The string: %s, %s%n", str, value.getClass().getName());
167        String patternString = "^0(\\.0+)?$";
168        Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
169        Matcher matcher = pattern.matcher(str);
170        if (matcher.matches()) {
171//                System.err.format("The string: '%s', result: %b%n", str, false);
172            return false;
173        }
174//            System.err.format("The string: '%s', result: %b%n", str, !str.isEmpty());
175        return !str.isEmpty();
176    }
177
178    /**
179     * Convert a value to a boolean by Jython rules.
180     * <P>
181     * Rules:
182     * null is converted to false
183     * empty string is converted to false
184     * "0" string is converted to false
185     * "0.000" string is converted to false, if the number of decimals is &gt; 0
186     * empty map is converted to false
187     * empty collection is converted to false
188     * An integer number is converted to false if the number is 0
189     * A floating number is converted to false if the number is -0.5 &lt; x &lt; 0.5
190     * Everything else is converted to true
191     * <P>
192     * For objects that implement the Reportable interface, the value is fetched
193     * from the method toReportString().
194     *
195     * @param value the value to convert
196     * @param do_i18n true if internationalization should be done, false otherwise
197     * @return the boolean value
198     */
199    public static boolean convertToBoolean_JythonRules(@CheckForNull Object value, boolean do_i18n) {
200        if (value == null) {
201            return false;
202        }
203
204        if (value instanceof Map) {
205            var map = ((Map<?,?>)value);
206            return !map.isEmpty();
207        }
208
209        if (value instanceof Collection) {
210            var collection = ((Collection<?>)value);
211            return !collection.isEmpty();
212        }
213
214        if (value.getClass().isArray()) {
215            var array = ((Object[])value);
216            return array.length > 0;
217        }
218
219        // JSON text node
220        if (value instanceof com.fasterxml.jackson.databind.node.TextNode) {
221            value = ((com.fasterxml.jackson.databind.node.TextNode)value).asText();
222        }
223
224        if (value instanceof Reportable) {
225            value = ((Reportable)value).toReportString();
226        }
227
228        if (value instanceof Number) {
229            double number = ((Number)value).doubleValue();
230            return ! ((-0.5 < number) && (number < 0.5));
231        } else if (value instanceof Boolean) {
232            return (Boolean)value;
233        } else {
234            if (value == null) return false;
235            return convertStringToBoolean_JythonRules(value.toString(), do_i18n);
236        }
237    }
238
239    private static long convertStringToLong(@Nonnull String str, boolean checkAll, boolean throwOnError, boolean warnOnError) {
240        String patternString = "^(\\-?\\d+)";
241        if (checkAll) patternString += "$";
242        Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
243        Matcher matcher = pattern.matcher(str);
244        // Only look at the beginning of the string
245        if (matcher.lookingAt()) {
246            String theNumber = matcher.group(1);
247            long number = Long.parseLong(theNumber);
248//            System.err.format("Number: %1.5f%n", number);
249            log.debug("the string {} is converted to the number {}", str, number);
250            return number;
251        } else {
252            if (warnOnError) {
253                log.warn("the string \"{}\" cannot be converted to a number", str);
254            }
255            if (throwOnError) {
256                throw new NumberFormatException(
257                        String.format("the string \"%s\" cannot be converted to a number", str));
258            }
259            return 0;
260        }
261    }
262
263    /**
264     * Convert a value to a long.
265     * <P>
266     * Rules:
267     * null is converted to 0
268     * empty string is converted to 0
269     * empty collection is converted to 0
270     * an instance of the interface Number is converted to the number
271     * a string that can be parsed as a number is converted to that number.
272     * a string that doesn't start with a digit is converted to 0
273     * <P>
274     * For objects that implement the Reportable interface, the value is fetched
275     * from the method toReportString() before doing the conversion.
276     *
277     * @param value the value to convert
278     * @return the long value
279     */
280    public static long convertToLong(@CheckForNull Object value) {
281        return convertToLong(value, false, false);
282    }
283
284    /**
285     * Convert a value to a long.
286     * <P>
287     * Rules:
288     * null is converted to 0
289     * empty string is converted to 0
290     * empty collection is converted to 0
291     * an instance of the interface Number is converted to the number
292     * a string that can be parsed as a number is converted to that number.
293     * a string that doesn't start with a digit is converted to 0
294     * <P>
295     * For objects that implement the Reportable interface, the value is fetched
296     * from the method toReportString() before doing the conversion.
297     *
298     * @param value the value to convert
299     * @param checkAll true if the whole string should be checked, false otherwise
300     * @param throwOnError true if a NumberFormatException should be thrown on error, false otherwise
301     * @return the long value
302     * @throws NumberFormatException on error if throwOnError is true
303     */
304    public static long convertToLong(@CheckForNull Object value, boolean checkAll, boolean throwOnError) {
305        return convertToLong(value, checkAll, throwOnError, true);
306    }
307
308    /**
309     * Convert a value to a long.
310     * <P>
311     * Rules:
312     * null is converted to 0
313     * empty string is converted to 0
314     * empty collection is converted to 0
315     * an instance of the interface Number is converted to the number
316     * a string that can be parsed as a number is converted to that number.
317     * a string that doesn't start with a digit is converted to 0
318     * <P>
319     * For objects that implement the Reportable interface, the value is fetched
320     * from the method toReportString() before doing the conversion.
321     *
322     * @param value the value to convert
323     * @param checkAll true if the whole string should be checked, false otherwise
324     * @param throwOnError true if a NumberFormatException should be thrown on error, false otherwise
325     * @param warnOnError true if a warning message should be logged on error
326     * @return the long value
327     * @throws NumberFormatException on error if throwOnError is true
328     */
329    public static long convertToLong(@CheckForNull Object value, boolean checkAll, boolean throwOnError, boolean warnOnError)
330            throws NumberFormatException {
331        if (value == null) {
332            log.warn("the object is null and the returned number is therefore 0.0");
333            return 0;
334        }
335
336        // JSON text node
337        if (value instanceof com.fasterxml.jackson.databind.node.TextNode) {
338            value = ((com.fasterxml.jackson.databind.node.TextNode)value).asText();
339        }
340
341        if (value instanceof Reportable) {
342            value = ((Reportable)value).toReportString();
343        }
344
345        if (value instanceof Number) {
346//            System.err.format("Number: %1.5f%n", ((Number)value).doubleValue());
347            if (!(value instanceof Byte) && !(value instanceof Short) && !(value instanceof Integer) && !(value instanceof Long)) {
348                if (throwOnError) {
349                    throw new NumberFormatException(
350                            String.format("the value %s cannot be converted to an integer", value));
351                }
352            }
353            return ((Number)value).longValue();
354        } else if (value instanceof Boolean) {
355            if (throwOnError) {
356                throw new NumberFormatException(
357                        String.format("the boolean value \"%b\" cannot be converted to an integer", ((Boolean)value)));
358            }
359            return ((Boolean)value) ? 1 : 0;
360        } else {
361            if (value == null) {
362                if (throwOnError) {
363                    throw new NumberFormatException(
364                            String.format("the null value cannot be converted to an integer"));
365                }
366                return 0;
367            }
368            return convertStringToLong(value.toString(), checkAll, throwOnError, warnOnError);
369        }
370    }
371
372    private static double convertStringToDouble(@Nonnull String str, boolean checkAll, boolean throwOnError, boolean warnOnError) {
373        String patternString = "^(\\-?\\d+(\\.\\d+)?(e\\-?\\d+)?)";
374        if (checkAll) patternString += "$";
375        Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
376        Matcher matcher = pattern.matcher(str);
377        // Only look at the beginning of the string
378        if (matcher.lookingAt()) {
379            String theNumber = matcher.group(1);
380            double number = Double.parseDouble(theNumber);
381//            System.err.format("Number: %1.5f%n", number);
382            log.debug("the string {} is converted to the number {}", str, number);
383            return number;
384        } else {
385            if (warnOnError) {
386                log.warn("the string \"{}\" cannot be converted to a number", str);
387            }
388            if (throwOnError) {
389                throw new NumberFormatException(
390                        String.format("the string \"%s\" cannot be converted to a number", str));
391            }
392            return 0.0d;
393        }
394    }
395
396    /**
397     * Convert a value to a double.
398     * <P>
399     * Rules:
400     * null is converted to 0
401     * empty string is converted to 0
402     * empty collection is converted to 0
403     * an instance of the interface Number is converted to the number
404     * a string that can be parsed as a number is converted to that number.
405     * if a string starts with a number AND do_i18n is false, it's converted to that number
406     * a string that doesn't start with a digit is converted to 0
407     * <P>
408     * For objects that implement the Reportable interface, the value is fetched
409     * from the method toReportString() before doing the conversion.
410     *
411     * @param value the value to convert
412     * @param do_i18n true if internationalization should be done, false otherwise
413     * @return the double value
414     */
415    public static double convertToDouble(@CheckForNull Object value, boolean do_i18n) {
416        return convertToDouble(value, do_i18n, false, false);
417    }
418
419    /**
420     * Convert a value to a double.
421     * <P>
422     * Rules:
423     * null is converted to 0
424     * empty string is converted to 0
425     * empty collection is converted to 0
426     * an instance of the interface Number is converted to the number
427     * a string that can be parsed as a number is converted to that number.
428     * if a string starts with a number AND do_i18n is false, it's converted to that number
429     * a string that doesn't start with a digit is converted to 0
430     * <P>
431     * For objects that implement the Reportable interface, the value is fetched
432     * from the method toReportString() before doing the conversion.
433     *
434     * @param value the value to convert
435     * @param do_i18n true if internationalization should be done, false otherwise
436     * @param checkAll true if the whole string should be checked, false otherwise
437     * @param throwOnError true if a NumberFormatException should be thrown on error, false otherwise
438     * @return the double value
439     * @throws NumberFormatException on error if throwOnError is true
440     */
441    public static double convertToDouble(@CheckForNull Object value, boolean do_i18n, boolean checkAll, boolean throwOnError) {
442        return convertToDouble(value, do_i18n, checkAll, throwOnError, true);
443    }
444
445    /**
446     * Convert a value to a double.
447     * <P>
448     * Rules:
449     * null is converted to 0
450     * empty string is converted to 0
451     * empty collection is converted to 0
452     * an instance of the interface Number is converted to the number
453     * a string that can be parsed as a number is converted to that number.
454     * if a string starts with a number AND do_i18n is false, it's converted to that number
455     * a string that doesn't start with a digit is converted to 0
456     * <P>
457     * For objects that implement the Reportable interface, the value is fetched
458     * from the method toReportString() before doing the conversion.
459     *
460     * @param value the value to convert
461     * @param do_i18n true if internationalization should be done, false otherwise
462     * @param checkAll true if the whole string should be checked, false otherwise
463     * @param throwOnError true if a NumberFormatException should be thrown on error, false otherwise
464     * @param warnOnError true if a warning message should be logged on error
465     * @return the double value
466     * @throws NumberFormatException on error if throwOnError is true
467     */
468    public static double convertToDouble(@CheckForNull Object value, boolean do_i18n, boolean checkAll, boolean throwOnError, boolean warnOnError) {
469        if (value == null) {
470            log.warn("the object is null and the returned number is therefore 0.0");
471            return 0.0d;
472        }
473
474        // JSON text node
475        if (value instanceof com.fasterxml.jackson.databind.node.TextNode) {
476            value = ((com.fasterxml.jackson.databind.node.TextNode)value).asText();
477        }
478
479        if (value instanceof Reportable) {
480            value = ((Reportable)value).toReportString();
481        }
482
483        if (value instanceof Number) {
484//            System.err.format("Number: %1.5f%n", ((Number)value).doubleValue());
485            return ((Number)value).doubleValue();
486        } else if (value instanceof Boolean) {
487            if (throwOnError) {
488                throw new NumberFormatException(
489                        String.format("the boolean value \"%b\" cannot be converted to a number", ((Boolean)value)));
490            }
491            return ((Boolean)value) ? 1 : 0;
492        } else {
493            if (value == null) {
494                if (throwOnError) {
495                    throw new NumberFormatException(
496                            String.format("the null value cannot be converted to a number"));
497                }
498                return 0.0;
499            }
500
501            if (do_i18n) {
502                // try to parse the string as a number
503                try {
504                    double number = IntlUtilities.doubleValue(value.toString());
505//                    System.err.format("The string: '%s', result: %1.4f%n", value, (float)number);
506                    return number;
507                } catch (ParseException ex) {
508                    log.debug("The string '{}' cannot be parsed as a number", value);
509                }
510            }
511            return convertStringToDouble(value.toString(), checkAll, throwOnError, warnOnError);
512        }
513    }
514
515    /**
516     * Convert a value to a String.
517     *
518     * @param value the value to convert
519     * @param do_i18n true if internationalization should be done, false otherwise
520     * @return the String value
521     */
522    @Nonnull
523    public static String convertToString(@CheckForNull Object value, boolean do_i18n) {
524        if (value == null) {
525            return "";
526        }
527
528        // JSON text node
529        if (value instanceof com.fasterxml.jackson.databind.node.TextNode) {
530            return ((com.fasterxml.jackson.databind.node.TextNode)value).asText();
531        }
532
533        if (value instanceof Reportable) {
534            return ((Reportable)value).toReportString();
535        }
536
537        if (value instanceof Number) {
538            if (do_i18n) {
539                return IntlUtilities.valueOf(((Number)value).doubleValue());
540            }
541        }
542
543        return value.toString();
544    }
545
546    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TypeConversionUtil.class);
547}