Source: josm/util.mjs

/**
 * Provides a set of static utility functions.
 *
 * @module josm/util
 */

/* global Java */

// -- imports
const MessageFormat = Java.type('java.text.MessageFormat')

/**
 * Checks whether a value is null or undefined.
 *
 * @param {object} value  the value to check
 * @return {boolean} false, if <code>value</code> is null or undefined; true, otherwise
 * @summary  Checks whether a value is null or undefined.
 * @function
 * @name isNothing
 */
export function isNothing(value) {
  return value === null || value === undefined
}

/**
 * Checks whether a value is neither null nor undefined.
 *
 * @param {object} value  the value to check
 * @return {boolean} false, if <code>value</code> is null or undefined; true, otherwise
 * @summary  Checks whether a value is neither null nor undefined.
 * @function
 * @name isSomething
 */
export function isSomething(val) {
  return !isNothing(val)
}

/**
 * Trims leading and trailing whitespace from <code>s</code>.
 * <p>
 *
 * Replies s, if s is null or undefined. Any other value is converted to a
 * string, then leading and trailing white space is removed.
 *
 * @param {string} s  the string to be trimmed
 * @return {string}
 * @summary  Trims leading and trailing whitespace from <code>s</code>.
 * @function
 * @name trim
 */
export function trim(s) {
  if (isNothing(s)) {
    return s
  }
  return (s + '').replace(/^\s+/, '').replace(/\s+$/, '')
}

/**
 * Assert a condition and throw an Error if the condition isn't met.
 *
 * <p><strong>Usage:</strong>
 * <dl>
 *   <dt><code>assert()</code></dt>
 *   <dd class="param-desc">Does nothing</dd>
 *
 *    <dt><code>assert(cond)</code></dt>
 *    <dd class="param-desc">Checks the condition <code>cond</code>. If it is false, throws an
 *    Error.</dd>
 *
 *    <dt><code>assert(cond, msg)</code></dt>
 *    <dd class="param-desc">Checks the condition <code>cond</code>. If it is false, throws an
 *    Error, whose <code>description</code> property
 *    is set to <code>msg</code>.</dd>
 *
 *      <dt><code>assert(cond, msg, objs...)</code></dt>
 *    <dd class="param-desc">Checks the condition <code>cond</code>. If it is false, throws an
 *    Error, whose <code>description</code> property
 *    is set to the formatted message <code>msg</code>. Internally uses
 *    <code>java.text.MessageFormat</code> to format the message.</dd>
 *
 * </dl>
 *
 * @example
 * import {assert} from 'josm/util'
 * // throws an Error
 * assert(false)
 *
 * // throws an Error e, with e.description == "My message"
 * assert(false, "My message")
 *
 * // throws an Error e, with e.description == "My message: test"
 * assert(false, "My message: {0}", "test")
 *
 * @summary  Assert a condition and throw an Error if the condition isn't met.
 * @function
 * @name assert
 * @static
 * @param {boolean} condition
 * @param {string} [message]  the message
 * @param {...object} [values] an optional list of values
 */
export function assert() {
  let message, name
  switch (arguments.length) {
    case 0:
      return

    case 1:
      if (arguments[0]) return
      name = 'AssertionError'
      message = 'An assertion failed'
      break

    case 2:
      if (arguments[0]) return
      name = 'AssertionError'
      message = arguments[1]
      break

    default: {
      if (arguments[0]) return
      name = 'AssertionError'
      const args = Array.prototype.slice.call(arguments, 0)
      message = MessageFormat.format(
        args[1],
        args.slice(2)
      )
      break
    }
  }
  const error = new Error(message)
  error.name = name
  throw error
}

/**
 * Asserts that <code>val</code> is defined and non-null.
 *
 * @example
 * import {assertSomething} from 'josm/util'
 * 
 * assertSomething(null)     // -> throws an exception
 * assertSomething(void 0)   // -> throws an exception
 *
 * assertSomting("test")     // -> OK
 * assertSomething(5)        // -> OK
 *
 * @param {any} val the value to check
 * @param {string} [msg] message if the assertion fails
 * @param {...object} [values] additional values used in <code>msg</code>
 *   placeholders
 * @summary  Asserts that <code>val</code> is defined and non-null.
 * @function
 * @static
 * @name assertSomething
 */
export function assertSomething(val) {
  let args
  if (arguments.length <= 1) {
    args = [isSomething(val),
      'Expected a defined non-null value, got {0}', val]
  } else {
    args = [isSomething(val)].concat(
      Array.prototype.slice.call(arguments, 1))
  }
  assert.apply(args)
}

/**
 * Asserts that <code>val</code> is a number.
 *
 * @param {Anything} value the value to check
 * @param {String} [msg]  message if the assertion fails
 * @param {...object} [values] values used in <code>msg</code> placeholders
 * @summary  Asserts that <code>val</code> is a number.
 * @function
 * @name assertNumber
 * @static
 */
export function assertNumber(val) {
  let args = []
  if (arguments.length <= 1) {
    args = [isSomething(val), 'Expected a number, got {0}', val]
  } else {
    args = [isSomething(val)]
      .concat(Array.prototype.slice.call(arguments, 1))
  }
  assert.apply(args)
}

/**
 * Returns true if  <code>val</code> is defined.
 *
 * @param {any} value the value to check
 * @summary  Returns true if  <code>val</code> is defined.
 * @return {boolean} true if  <code>val</code> is defined
 * @function
 * @name isDef
 * @static
  */
export function isDef(val) {
  return val !== undefined
}

/**
 * Returns true if  <code>val</code> is a number.
 *
 * @param {any} value the value to check
 * @summary  Returns true if  <code>val</code> is a number.
 * @return {boolean} true if  <code>val</code> is a number
 * @function
 * @name isNumber
 * @static
 */
export function isNumber(val) {
  return typeof val === 'number'
}

/**
 * Returns true if  <code>val</code> is a string.
 *
 * @param {any} value the value to check
 * @return {boolean} true, if val is a string or a String object
 * @summary Returns true if  <code>val</code> is a string.
 * @function
 * @name isString
 * @static
 */
export function isString(val) {
  return isDef(val) &&
    (typeof val === 'string' || val instanceof String)
}

/**
 * Replies true if <code>val</code> is an array.
 *
 * @param {anything} value the value to check
 * @return {boolean} true, if val is an array
 * @summary Replies true if <code>val</code> is an array.
 * @function
 * @name isArray
 * @static
 */
export function isArray(val) {
  return Object.prototype.toString.call(val) === '[object Array]'
}

/**
 * Replies true if <code>val</code> is a list of arguments.
 *
 * @param {anything} value the value to check
 * @return {boolean} true, if val is a list of arguments
 * @summary Replies true if <code>val</code> is a list of arguments.
 * @function
 * @name isArguments
 * @static
 */
export function isArguments(val) {
  return Object.prototype.toString.call(val) === '[object Arguments]'
}

/**
 * Replies the number of properties owned by <code>o</code>.
 *
 * @example
 * import {countProperties} from 'josm/util'
 * 
 * let o = {p1: "v1", p2: "v2"}
 * let c = countProperties(o)   // ->  2
 *
 * o = {}
 * c = countProperties(o)       // ->  0
 *
 * o = undefined
 * c = countProperties(o)       // ->  undefined
 *
 * @param {any} o the object
 * @summary Replies the number of properties owned by <code>o</code>.
 * @return {number} the number of properties owned by <code>o</code>.
 * @function
 * @name countProperties
 * @static
 */
export function countProperties(o) {
  if (isNothing(o)) return undefined
  if (!(typeof o === 'object')) return undefined
  let count = 0
  for (const p in o) {
    if (hasProp(o, p)) count++
  }
  return count
}

/**
 * Replies true, if <code>o</code> owns at least one property.
 *
 * @example
 * import {hasProperties} from 'josm/util'
 * 
 * let o = {p1: "v1", p2: "v2"}
 * let c = hasProperties(o)         // ->  true
 *
 * o = {}
 * c = hasProperties(o)             // ->  false
 *
 * o = undefined
 * c = hasProperties(o)             // ->  false
 *
 * @param {any} o the object
 * @summary Replies true, if <code>o</code> owns at least one property.
 * @return {boolean} true, if <code>o</code> owns at least one property.
 * @function
 * @name hasProperties
 * @static
 */
export function hasProperties(o) {
  const count = countProperties(o)
  if (count === undefined) return false
  return count > 0
}

/**
 * Replies true, if f is a function.
 *
 * @param {any} f the object
 * @summary Replies true, if f is a function.
 * @return {boolean} true, if f is a function.
 * @function
 * @name isFunction
 * @static
 */
export function isFunction(f) {
  return typeof f === 'function'
}

/**
 * Mixes the properties of a list of objects into one object.
 *
 * @return a new object which includes the combined properties of the
 *         argument objects
 * @return {object}
 * @summary Mixes the properties of a list of objects into one object.
 * @function
 * @name mix
 * @static
*/
export function mix() {
  const mixin = {}

  function copyProperties (other) {
    for (const p in other) {
      if (!hasProp(other, p)) continue
      mixin[p] = other[p]
    }
  }

  for (let i = 0; i < arguments.length; i++) {
    const template = arguments[i]
    if (isNothing(template)) continue
    if (!(typeof template === 'object')) continue
    copyProperties(template)
  }
  return mixin
}

/**
 * Prints a message to stdout (including newline).
 *
 * Supports the same string templates as {@class java.text.MessageFormat}.
 *
 * @example
 * import * as util from 'josm/util'
 * 
 * const myname = '...'
 * util.println('Hello world! My name is {0}', myname);
 * // escape meta characters like {, } or ' with a leading apostrophe
 * util.println(" a pair of curly braces '{'}");
 *
 * @summary Prints a message to stdout (including newline).
 * @param {string} message
 * @param {...object} [args]
 * @function
 * @name println
 * @static
 */
export function println() {
  const args = Array.prototype.slice.call(arguments, 0)
  if (args.length === 0) return ''
  args[0] = args[0] + '' // make sure first argument is a string
  const System = Java.type('java.lang.System')
  System.out.println(MessageFormat.format(args[0], args.slice(1)))
}

/**
 * Prints a message to stdout (without newline).
 *
 * Supports the same string templates as {@class java.text.MessageFormat}
 *
 * @example
 * import * as util from 'josm/util'
 * 
 * const  myname = "..."
 * util.print('Hello world! My name is {0}', myname)
 * // escape meta characters like {, } or ' with a leading apostrophe
 * util.print(" a pair of curly braces '{'}")
 *
 * @summary Prints a message to stdout (without newline).
 * @param {string} message
 * @param {...object} [args]
 * @function
 * @name print
 * @static
 */
export function print() {
  const args = Array.prototype.slice.call(arguments, 0)
  if (args.length === 0) return ''
  args[0] = args[0] + '' // make sure first argument is a string
  const System = Java.type('java.lang.System')
  System.out.print(MessageFormat.format(args[0], args.slice(1)))
}

/**
 * Checks whether two java objects are either both null or equal by calling
 * o1.equals(o2).
 *
 * @param {object} o1 a java object or null
 * @param {object} o2 a java object or null
 * @summary Are two java objects equal.
 * @return {boolean}
 * @function
 * @name javaEquals
 * @static
 */
export function javaEquals(o1, o2) {
  assert(typeof o1 === 'object' && typeof o1.equals === 'function')
  assert(typeof o2 === 'object' && typeof o2.equals === 'function')
  if (o1 === null && o2 === null) return true
  if (o1 === null && o2 !== null) return false
  return o1.equals(o2)
}

/**
 * Iterates over the elements in <code>collection</code> and invokes
 * <code>delegate()</code> on each element.
 *
 * @param {array|arguments|java.util.Collection} collection the collection of
 *   elements
 * @param {function} delegate the function to call on each elemennt
 * @summary Iteraties over the elements of a collection
 * @function
 * @name each
 * @static
 */
export function each(collection, delegate) {
  const Collection = Java.type('java.util.Collection')
  if (isNothing(collection) || isNothing(delegate)) return
  if (isArray(collection) || isArguments(collection)) {
    const len = collection.length
    for (let i = 0; i < len; i++) delegate(collection[i])
  } else if (collection instanceof Collection) {
    for (let it = collection.iterator(); it.hasNext();) delegate(it.next())
  } else {
    assert(false, 'collection: unexpected type of value, got {0}"',
      collection)
  }
}

/**
 * Replies true, if a value is an array, an arguments list or a Java
 * collection.
 *
 * @param {object} value the value to check
 * @summary Is a value a collection?
 * @return {boolean} true, if <code>value</code> is a collection
 * @function
 * @name isCollection
 * @static
 */
export function isCollection(value) {
  const Collection = Java.type('java.util.Collection')
  return isArray(value) ||
    isArguments(value) ||
    value instanceof Collection
}

export function hasProp(o, name) {
  return Object.prototype.hasOwnProperty.call(o, name)
}