Strings.kt

  1. package com.hexagonkt.core.text

  2. import com.hexagonkt.core.urlOf
  3. import java.io.ByteArrayInputStream
  4. import java.io.File
  5. import java.io.InputStream
  6. import java.net.InetAddress
  7. import java.net.URI
  8. import java.net.URL
  9. import java.text.Normalizer
  10. import java.text.Normalizer.Form.NFD
  11. import java.time.LocalDate
  12. import java.time.LocalDateTime
  13. import java.time.LocalTime
  14. import java.util.Base64
  15. import kotlin.reflect.KClass

  16. private const val VARIABLE_PREFIX = "{{"
  17. private const val VARIABLE_SUFFIX = "}}"

  18. private val base64Encoder: Base64.Encoder by lazy { Base64.getEncoder().withoutPadding() }
  19. private val base64Decoder: Base64.Decoder by lazy { Base64.getDecoder() }

  20. /** Runtime specific end of line. */
  21. val eol: String by lazy { System.lineSeparator() }

  22. /** Supported types for the [parseOrNull] function. */
  23. val parsedClasses: Set<KClass<*>> by lazy {
  24.     setOf(
  25.         Boolean::class,
  26.         Int::class,
  27.         Long::class,
  28.         Float::class,
  29.         Double::class,
  30.         String::class,
  31.         InetAddress::class,
  32.         URL::class,
  33.         URI::class,
  34.         File::class,
  35.         LocalDate::class,
  36.         LocalTime::class,
  37.         LocalDateTime::class,
  38.     )
  39. }

  40. /**
  41.  * Filter the target string substituting each key by its value. The keys format resembles Mustache's
  42.  * one: `{{key}}` and all occurrences are replaced by the supplied value.
  43.  *
  44.  * If a variable does not have a parameter, it is left as it is.
  45.  *
  46.  * @param parameters The map with the list of key/value tuples.
  47.  * @return The filtered text or the same string if no values are passed or found in the text.
  48.  * @sample com.hexagonkt.core.text.StringsTest.filterVarsExample
  49.  */
  50. fun String.filterVars(parameters: Map<*, *>): String =
  51.     this.filter(
  52.         VARIABLE_PREFIX,
  53.         VARIABLE_SUFFIX,
  54.         parameters
  55.             .filterKeys { it != null }
  56.             .map { (k, v) -> k.toString() to v.toString() }
  57.             .toMap()
  58.     )

  59. /**
  60.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  61.  *
  62.  * @receiver .
  63.  * @param prefix .
  64.  * @param suffix .
  65.  * @param parameters .
  66.  * @return .
  67.  */
  68. fun String.filter(prefix: String, suffix: String, parameters: Map<String, *>): String =
  69.     parameters.entries.fold(this) { result, (first, second) ->
  70.         result.replace(prefix + first + suffix, second.toString())
  71.     }

  72. /**
  73.  * Encode the content of this byteArray to base64.
  74.  *
  75.  * @receiver ByteArray to be encoded to base64.
  76.  * @return The base64 encoded string.
  77.  */
  78. fun ByteArray.encodeToBase64(): String =
  79.     base64Encoder.encodeToString(this)

  80. /**
  81.  * Encode this string to base64.
  82.  *
  83.  * @receiver String to be encoded to base64.
  84.  * @return The base64 encoded string.
  85.  */
  86. fun String.encodeToBase64(): String =
  87.     toByteArray().encodeToBase64()

  88. /**
  89.  * Decode this base64 encoded string.
  90.  *
  91.  * @receiver String encoded to base64.
  92.  * @return The ByteArray result of decoding the base64 string.
  93.  */
  94. fun String.decodeBase64(): ByteArray =
  95.     base64Decoder.decode(this)

  96. /**
  97.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  98.  *
  99.  * @receiver .
  100.  * @param T .
  101.  * @param type .
  102.  * @return .
  103.  */
  104. @Suppress("UNCHECKED_CAST") // All allowed types are checked at runtime
  105. fun <T : Any> String.parse(type: KClass<T>): T =
  106.     this.let {
  107.         require(type in parsedClasses) { "Unsupported type: ${type.qualifiedName}" }

  108.         when (type) {
  109.             Boolean::class -> this.toBooleanStrictOrNull()
  110.             Int::class -> this.toIntOrNull()
  111.             Long::class -> this.toLongOrNull()
  112.             Float::class -> this.toFloatOrNull()
  113.             Double::class -> this.toDoubleOrNull()
  114.             String::class -> this
  115.             InetAddress::class -> this.let(InetAddress::getByName)
  116.             URL::class -> this.let(::urlOf)
  117.             URI::class -> this.let(::URI)
  118.             File::class -> this.let(::File)
  119.             LocalDate::class -> LocalDate.parse(this)
  120.             LocalTime::class -> LocalTime.parse(this)
  121.             LocalDateTime::class -> LocalDateTime.parse(this)
  122.             else -> error("Unsupported type: ${type.qualifiedName}")
  123.         }
  124.     } as T

  125. /**
  126.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  127.  *
  128.  * @receiver .
  129.  * @param T .
  130.  * @param type .
  131.  * @return .
  132.  */
  133. fun <T : Any> String?.parseOrNull(type: KClass<T>): T? =
  134.     this?.let {
  135.         require(type in parsedClasses) { "Unsupported type: ${type.qualifiedName}" }
  136.         try {
  137.             parse(type)
  138.         }
  139.         catch (e: Exception) {
  140.             null
  141.         }
  142.     }

  143. fun String.stripAnsi(): String =
  144.     replace(Ansi.REGEX, "")

  145. /**
  146.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  147.  *
  148.  * @receiver .
  149.  * @return .
  150.  */
  151. fun String.toStream(): InputStream =
  152.     ByteArrayInputStream(this.toByteArray())

  153. /**
  154.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  155.  *
  156.  * @receiver .
  157.  * @param count .
  158.  * @param pad .
  159.  * @return .
  160.  */
  161. fun String.prependIndent(count: Int = 4, pad: String = " "): String =
  162.     this.prependIndent(pad.repeat(count))

  163. /**
  164.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  165.  *
  166.  * @receiver .
  167.  * @param T .
  168.  * @param converter .
  169.  * @return .
  170.  */
  171. fun <T : Enum<*>> String.toEnum(converter: (String) -> T): T =
  172.     uppercase().replace(" ", "_").let(converter)

  173. /**
  174.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  175.  *
  176.  * @receiver .
  177.  * @param T .
  178.  * @param converter .
  179.  * @return .
  180.  */
  181. fun <T : Enum<*>> String.toEnumOrNull(converter: (String) -> T): T? =
  182.     try {
  183.         toEnum(converter)
  184.     }
  185.     catch (e: IllegalArgumentException) {
  186.         null
  187.     }

  188. /**
  189.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  190.  *
  191.  * @receiver .
  192.  * @param text .
  193.  * @return .
  194.  */
  195. fun Regex.findGroups(text: String): List<MatchGroup> =
  196.     (this.find(text)?.groups ?: emptyList<MatchGroup>())
  197.         .filterNotNull()
  198.         .drop(1)

  199. /**
  200.  * Format the string as a banner with a delimiter above and below text. The character used to
  201.  * render the delimiter is defined.
  202.  *
  203.  * @param bannerDelimiter Delimiter char for banners.
  204.  */
  205. fun String.banner(bannerDelimiter: String = "*"): String =
  206.     bannerDelimiter
  207.         .repeat(this
  208.             .lines()
  209.             .asSequence()
  210.             .map { it.length }
  211.             .maxOrElse(0)
  212.         )
  213.         .let { "$it$eol$this$eol$it" }

  214. // TODO Add `box` (create a rectangle text) and doubleSpace (add a space between letters)
  215. // TODO These and other implemented methods can fit in a Effects.kt file

  216. /**
  217.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  218.  *
  219.  * @receiver .
  220.  * @return .
  221.  */
  222. fun String.stripAccents(): String =
  223.     Normalizer.normalize(this, NFD).replace("\\p{M}".toRegex(), "")

  224. /**
  225.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  226.  *
  227.  * @param bytes .
  228.  * @return .
  229.  */
  230. fun utf8(vararg bytes: Int): String =
  231.     String(bytes.map(Int::toByte).toByteArray())

  232. fun String.toEnumValue(): String =
  233.     trim().uppercase().replace(" ", "_")

  234. internal fun Sequence<Int>.maxOrElse(fallback: Int): Int =
  235.     this.maxOrNull() ?: fallback