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 > 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 < x < 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 > 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 < x < 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}