This is an automated email from the ASF dual-hosted git repository.
aradzinski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git
The following commit(s) were added to refs/heads/master by this push:
new df60a53 WIP migration
df60a53 is described below
commit df60a53e91dbc3f9de27d3770eefb8bf59ac1e77
Author: Aaron Radzinski <[email protected]>
AuthorDate: Tue Oct 5 11:32:35 2021 -0700
WIP migration
---
.../org/apache/nlpcraft/common/NCException.java | 42 ++
.../nlpcraft/common/ascii/NCAsciiTable.scala | 639 +++++++++++++++++++++
.../org/apache/nlpcraft/common/util/NCUtils.scala | 132 +++++
3 files changed, 813 insertions(+)
diff --git
a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/NCException.java
b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/NCException.java
new file mode 100644
index 0000000..157ebb0
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/NCException.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.common;
+
+/**
+ * Base NLPCraft exception.
+ */
+public class NCException extends RuntimeException {
+ /**
+ * Creates new exception with given parameters.
+ *
+ * @param msg Error message.
+ * @param cause Optional cause of this exception.
+ */
+ public NCException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Creates new exception with given error message.
+ *
+ * @param msg Error message.
+ */
+ public NCException(String msg) {
+ super(msg);
+ }
+}
\ No newline at end of file
diff --git
a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/ascii/NCAsciiTable.scala
b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/ascii/NCAsciiTable.scala
new file mode 100644
index 0000000..067093f
--- /dev/null
+++
b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/ascii/NCAsciiTable.scala
@@ -0,0 +1,639 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.common.ascii
+
+import java.io.{IOException, PrintStream}
+import java.util.List as JList
+import com.typesafe.scalalogging.Logger
+import org.apache.nlpcraft.common.*
+import org.apache.nlpcraft.common.ascii.NCAsciiTable.*
+import org.apache.nlpcraft.common.ansi.NCAnsi.*
+import org.apache.nlpcraft.common.util.NCUtils
+
+import scala.collection.mutable
+import scala.jdk.CollectionConverters.CollectionHasAsScala
+import scala.util.Using
+
+/**
+ * `ASCII`-based table with minimal styling support.
+ */
+class NCAsciiTable:
+ /**
+ * Cell style.
+ */
+ private sealed case class Style(
+ var leftPad: Int = 1, // >= 0
+ var rightPad: Int = 1, // >= 0
+ var maxWidth: Int = Int.MaxValue, // > 0
+ var align: String = "center" // center, left, right
+ ):
+ /** Gets overall padding (left + right). */
+ def padding: Int = leftPad + rightPad
+
+
+ /**
+ * Cell style.
+ */
+ private object Style:
+ /**
+ *
+ * @param sty Style text.
+ */
+ def apply(sty: String): Style =
+ val cs = new Style
+
+ if sty.nonEmpty then
+ for (e <- sty.split(','))
+ val a = e.split(":")
+ require(a.length == 2, s"Invalid cell style: ${e.trim}")
+ val a0 = a(0).trim
+ val a1 = a(1).trim
+
+ a0 match
+ case "leftPad" => cs.leftPad = a1.toInt
+ case "rightPad" => cs.rightPad = a1.toInt
+ case "maxWidth" => cs.maxWidth = a1.toInt
+ case "align" => cs.align = a1.toLowerCase
+ case _ => assert(assertion = false, s"Invalid style:
${e.trim}")
+
+ require(cs.leftPad >= 0, "Style 'leftPad' must >= 0.")
+ require(cs.rightPad >= 0, "Style 'rightPad' must >= 0.")
+ require(cs.maxWidth > 0, "Style 'maxWidth' must > 0.")
+ require(cs.align == "center" || cs.align == "left" || cs.align ==
"right", "Style 'align' must be 'left', 'right' or 'center'.")
+
+ cs
+
+ /**
+ * Cell holder.
+ *
+ * @param style
+ * @param lines Lines that are already cut up per `style`, if required.
+ */
+ private sealed case class Cell(style: Style, lines: Seq[String]):
+ // Cell's calculated width including padding.
+ lazy val width: Int = style.padding + (if (height > 0)
lines.map(NCUtils.stripAnsi(_).length).max else 0)
+ // Gets height of the cell.
+ lazy val height: Int = lines.length
+
+ /**
+ * Margin holder.
+ */
+ private sealed case class Margin(
+ top: Int = 0,
+ right: Int = 0,
+ bottom: Int = 0,
+ left: Int = 0
+ )
+
+ // Table drawing symbols.
+ private val HDR_HOR = ansi256Fg(105, "=")
+ private val HDR_VER = ansi256Fg(105, "|")
+ private val HDR_CRS = ansi256Fg(99, "+")
+ private val ROW_HOR = ansi256Fg(39, "-")
+ private val ROW_VER = ansi256Fg(39, "|")
+ private val ROW_CRS = ansi256Fg(202, "+")
+ // Headers & rows.
+ private var hdr = IndexedSeq.empty[Cell]
+ private var rows = IndexedSeq.empty[IndexedSeq[Cell]]
+ // Current row, if any.
+ private var curRow: IndexedSeq[Cell] = _
+ // Table's margin, if any.
+ private var margin = Margin()
+
+ /**
+ * Global flag indicating whether or not to draw inside horizontal lines
+ * between individual rows.
+ */
+ var insideBorder = false
+ /**
+ * Global Flag indicating whether of not to automatically draw horizontal
lines
+ * for multiline rows.
+ */
+ var multiLineAutoBorder = true
+ /**
+ * If lines exceeds the style's maximum width it will be broken up
+ * either by nearest space (by whole words) or mid-word.
+ */
+ var breakUpByWords = true
+ /** Default row style. */
+ var defaultRowStyle: String = DFLT_ROW_STYLE
+ /** Default header style. */
+ var defaultHeaderStyle: String = DFLT_HEADER_STYLE
+
+ private def dash(ch: String, len: Int): String = ch * len
+ private def space(len: Int): String = " " * len
+
+ /**
+ * Sets table's margin.
+ *
+ * @param top Top margin.
+ * @param right Right margin.
+ * @param bottom Bottom margin.
+ * @param left Left margin.
+ */
+ def margin(top: Int = 0, right: Int = 0, bottom: Int = 0, left: Int = 0):
NCAsciiTable =
+ margin = Margin(top, right, bottom, left)
+ this
+
+ /**
+ * Starts data row.
+ */
+ def startRow(): Unit =
+ curRow = IndexedSeq.empty[Cell]
+
+ /**
+ * Ends data row.
+ */
+ def endRow(): Unit =
+ rows :+= curRow
+ curRow = null
+
+ /**
+ * Adds row (one or more row cells).
+ *
+ * @param cells Row cells. For multi-line cells - use `Seq(...)`.
+ */
+ def +=(cells: Any*): NCAsciiTable =
+ startRow()
+ cells foreach {
+ case i: Iterable[_] => addRowCell(i.iterator.toSeq: _*)
+ case a => addRowCell(a)
+ }
+ endRow()
+ this
+
+ /**
+ * Adds row (one or more row cells).
+ *
+ * @param cells Row cells. For multi-line cells - use `Seq(...)`.
+ */
+ def +=(cells: mutable.Seq[Any]): NCAsciiTable =
+ +=(cells.toSeq: _*)
+
+ /**
+ * Adds row (one or more row cells) with a given style.
+ *
+ * @param cells Row cells tuples (style, text). For multi-line cells -
use `Seq(...)`.
+ */
+ def +/(cells: (String, Any)*): NCAsciiTable =
+ startRow()
+ cells foreach {
+ case i if i._2.isInstanceOf[Iterable[_]] => addStyledRowCell(i._1,
i._2.asInstanceOf[Iterable[_]].iterator.toSeq: _*)
+ case a => addStyledRowCell(a._1, a._2)
+ }
+ endRow()
+ this
+
+ /**
+ * Adds row.
+ *
+ * @param cells Row cells.
+ */
+ def addRow(cells: JList[Any]): NCAsciiTable =
+ startRow()
+ cells.asScala.foreach(p => addRowCell(p))
+ endRow()
+ this
+
+
+ /**
+ * Adds header (one or more header cells).
+ *
+ * @param cells Header cells. For multi-line cells - use `Seq(...)`.
+ */
+ def #=(cells: Any*): NCAsciiTable =
+ cells foreach {
+ case i: Iterable[_] => addHeaderCell(i.iterator.toSeq: _*)
+ case a => addHeaderCell(a)
+ }
+ this
+
+ /**
+ * Adds header (one or more header cells).
+ *
+ * @param cells Header cells. For multi-line cells - use `Seq(...)`.
+ */
+ def #=(cells: mutable.Seq[Any]): NCAsciiTable =
+ #=(cells.toSeq: _*)
+
+ /**
+ * Adds styled header (one or more header cells).
+ *
+ * @param cells Header cells tuples (style, text). For multi-line cells -
use `Seq(...)`.
+ */
+ def #/(cells: (String, Any)*): NCAsciiTable =
+ cells foreach {
+ case i if i._2.isInstanceOf[Iterable[_]] =>
addStyledHeaderCell(i._1, i._2.asInstanceOf[Iterable[_]].iterator.toSeq: _*)
+ case a => addStyledHeaderCell(a._1, a._2)
+ }
+ this
+
+ /**
+ * Adds headers.
+ *
+ * @param cells Header cells.
+ */
+ def addHeaders(cells: JList[Any]): NCAsciiTable =
+ cells.asScala.foreach(addHeaderCell(_))
+ this
+
+ /**
+ * Adds headers with the given `style`.
+ *
+ * @param style Style top use.
+ * @param cells Header cells.
+ */
+ def addStyledHeaders(style: String, cells: JList[Any]): NCAsciiTable =
+ cells.asScala.foreach(addHeaderCell(style, _))
+ this
+
+ // Handles the 'null' strings.
+ private def x(s: Any): String = s match
+ case null => "<null>"
+ case _ => s.toString
+
+ /**
+ *
+ * @param maxWidth
+ * @param lines
+ * @return
+ */
+ private def breakUpByNearestSpace(maxWidth: Int, lines: Seq[String]):
Seq[String] =
+ lines.flatMap(line => {
+ if line.isEmpty then
+ mutable.Buffer("")
+ else
+ val leader = line.indexWhere(_ != ' ') // Number of leading
spaces.
+
+ val buf = mutable.Buffer.empty[String]
+
+ var start = 0
+ var lastSpace = -1
+ var curr = 0
+ val len = line.length
+
+ def addLine(s: String): Unit = buf += (if (buf.isEmpty) s else
space(leader) + s)
+
+ while (curr < len)
+ if curr - start > maxWidth then
+ val end = if (lastSpace == -1) curr else lastSpace + 1
/* Keep space at the end of the line. */
+ addLine(line.substring(start, end))
+ start = end
+ if line.charAt(curr) == ' ' then
+ lastSpace = curr
+
+ curr += 1
+
+ if start < len then
+ val lastLine = line.substring(start)
+ if lastLine.trim.nonEmpty then addLine(lastLine)
+
+ buf
+ })
+
+ /**
+ *
+ * @param hdr
+ * @param style
+ * @param lines
+ * @return
+ */
+ private def mkStyledCell(hdr: Boolean, style: String, lines: Any*): Cell =
+ val st = Style(style)
+ var strLines = lines.map(x)
+
+ if hdr then strLines = strLines.map(s => s"$ansiBlueFg$s$ansiReset")
+
+ Cell(
+ st,
+ if breakUpByWords then
+ breakUpByNearestSpace(st.maxWidth, strLines)
+ else
+ (for (str <- strLines) yield str.grouped(st.maxWidth)).flatten
+ )
+
+
+ /**
+ * Adds single header cell with the default style..
+ *
+ * @param lines One or more cell lines.
+ */
+ def addHeaderCell(lines: Any*): NCAsciiTable =
+ hdr :+= mkStyledCell(
+ true,
+ defaultHeaderStyle,
+ lines: _*
+ )
+ this
+
+ /**
+ * Adds single row cell with the default style.
+ *
+ * @param lines One or more row cells. Multiple lines will be printed on
separate lines.
+ */
+ def addRowCell(lines: Any*): NCAsciiTable =
+ curRow :+= mkStyledCell(
+ false,
+ defaultRowStyle,
+ lines: _*
+ )
+ this
+
+ /**
+ * Adds single header cell with the default style..
+ *
+ * @param style Style to use.
+ * @param lines One or more cell lines.
+ */
+ def addStyledHeaderCell(style: String, lines: Any*): NCAsciiTable =
+ hdr :+= mkStyledCell(
+ hdr = true,
+ if style.trim.isEmpty then defaultHeaderStyle else style,
+ lines: _*
+ )
+ this
+
+ /**
+ * Adds single row cell with the default style.
+ *
+ * @param style Style to use.
+ * @param lines One or more row cells. Multiple lines will be printed on
separate lines.
+ */
+ def addStyledRowCell(style: String, lines: Any*): NCAsciiTable =
+ curRow :+= mkStyledCell(
+ false,
+ if style.trim.isEmpty then defaultRowStyle else style,
+ lines: _*
+ )
+ this
+
+ /**
+ *
+ * @param txt Text to align.
+ * @param width Width already accounts for padding.
+ * @param sty Style.
+ */
+ private def aligned(txt: String, width: Int, sty: Style): String =
+ val d = width - NCUtils.stripAnsi(txt).length
+ sty.align match
+ case "center" => space(d / 2) + txt + space(d / 2 + d % 2)
+ case "left" => space(sty.leftPad) + txt + space(d - sty.leftPad)
+ case "right" => space(d - sty.rightPad) + txt + space(sty.rightPad)
+ case _ => throw new AssertionError(s"Invalid align option: $sty")
+
+ override def toString: String = mkString
+
+ /**
+ * Prepares output string.
+ */
+ private def mkString: String =
+ // Make sure table is not empty.
+ if (hdr.isEmpty && rows.isEmpty) then return ""
+
+ var colsNum = -1
+ val isHdr = hdr.nonEmpty
+
+ if isHdr then colsNum = hdr.size
+
+ // Calc number of columns and make sure all rows are even.
+ for (r <- rows)
+ if colsNum == -1 then colsNum = r.size
+ else if colsNum != r.size then assert(assertion = false, "Table
with uneven rows.")
+
+ assert(colsNum > 0, "No columns found.")
+
+ // At this point all rows in the table have the
+ // the same number of columns.
+ val colWidths = new Array[Int](colsNum) // Column widths.
+ val rowHeights = new Array[Int](rows.length) // Row heights.
+ // Header height.
+ var hdrH = 0
+
+ // Initialize column widths with header row (if any).
+ for (i <- hdr.indices)
+ val c = hdr(i)
+ colWidths(i) = c.width
+ hdrH = math.max(hdrH, c.height)
+
+ // Calculate row heights and column widths.
+ for (i <- rows.indices; j <- 0 until colsNum)
+ val c = rows(i)(j)
+ rowHeights(i) = math.max(rowHeights(i), c.height)
+ colWidths(j) = math.max(colWidths(j), c.width)
+
+ // Table width without the border.
+ val tableW = colWidths.sum + colsNum - 1
+ val tbl = new StringBuilder
+ // Top margin.
+ for (_ <- 0 until margin.top)
+ tbl ++= " \n"
+
+ /**
+ *
+ * @param crs
+ * @param cor
+ * @return
+ */
+ def mkAsciiLine(crs: String, cor: String): String =
+ s"${space(margin.left)}$crs${dash(cor,
tableW)}$crs${space(margin.right)}\n"
+
+ // Print header, if any.
+ if isHdr then
+ tbl ++= mkAsciiLine(HDR_CRS, HDR_HOR)
+ for (i <- 0 until hdrH)
+ // Left margin and '|'.
+ tbl ++= s"${space(margin.left)}$HDR_VER"
+
+ for (j <- hdr.indices)
+ val c = hdr(j)
+ if i >= 0 && i < c.height then
+ tbl ++= aligned(c.lines(i), colWidths(j), c.style)
+ else
+ tbl ++= space(colWidths(j))
+ tbl ++= s"$HDR_VER" // '|'
+
+ // Right margin.
+ tbl ++= s"${space(margin.right)}\n"
+
+ tbl ++= mkAsciiLine(HDR_CRS, HDR_HOR)
+
+ else
+ tbl ++= mkAsciiLine(ROW_CRS, ROW_HOR)
+
+ val tblWidthLine = s"${space(margin.left)}$ROW_CRS${dash(ROW_HOR,
tableW)}$ROW_CRS${space(margin.right)}\n"
+
+ // Print rows, if any.
+ if rows.nonEmpty then
+ val addHorLine = (i: Int) => {
+ // Left margin and '+'
+ tbl ++= s"${space(margin.left)}$ROW_CRS"
+ for (k <- rows(i).indices)
+ tbl ++= s"${dash(ROW_HOR, colWidths(k))}$ROW_CRS"
+ // Right margin.
+ tbl ++= s"${space(margin.right)}\n"
+ }
+
+ for (i <- rows.indices)
+ val row = rows(i)
+ val rowH = rowHeights(i)
+ for (j <- 0 until rowH)
+ // Left margin and '|'
+ tbl ++= s"${space(margin.left)}$ROW_VER"
+
+ for (k <- row.indices)
+ val c = row(k)
+ val w = colWidths(k)
+ if j < c.height then
+ tbl ++= aligned(c.lines(j), w, c.style)
+ else
+ tbl ++= space(w)
+ tbl ++= s"$ROW_VER" // '|'
+
+
+ // Right margin.
+ tbl ++= s"${space(margin.right)}\n"
+
+ if (i < rows.size - 1 && ((rowH > 1 && multiLineAutoBorder) ||
insideBorder))
+ addHorLine(i)
+
+ tbl ++= tblWidthLine
+
+ // Bottom margin.
+ for (_ <- 1 to margin.bottom)
+ tbl ++= s" \n"
+ val res = tbl.toString
+ res.substring(0, res.length - 1)
+
+ /**
+ * Prepares table string representation for logger.
+ * @param header Optional header.
+ */
+ private def mkLogString(header: Option[String] = None): String =
s"${header.getOrElse("")}\n$mkString"
+
+ /**
+ * Renders this table to log as debug.
+ *
+ * @param log Logger.
+ * @param header Optional header.
+ */
+ def debug(log: Logger, header: Option[String] = None): Unit =
log.debug(mkLogString(header))
+
+ /**
+ * Renders this table to log as info.
+ *
+ * @param log Logger.
+ * @param header Optional header.
+ */
+ def info(log: Logger, header: Option[String] = None): Unit =
log.info(mkLogString(header))
+
+ /**
+ * Renders this table to log as warn.
+ *
+ * @param log Logger.
+ * @param header Optional header.
+ */
+ def warn(log: Logger, header: Option[String] = None): Unit =
log.warn(mkLogString(header))
+
+ /**
+ * Renders this table to log as error.
+ *
+ * @param log Logger.
+ * @param header Optional header.
+ */
+ def error(log: Logger, header: Option[String] = None): Unit =
log.error(mkLogString(header))
+
+ /**
+ * Renders this table to log as trace.
+ *
+ * @param log Logger.
+ * @param header Optional header.
+ */
+ def trace(log: Logger, header: Option[String] = None): Unit =
log.trace(mkLogString(header))
+
+ /**
+ * Renders this table to output stream.
+ *
+ * @param ps Output stream.
+ */
+ def render(ps: PrintStream = System.out): Unit = ps.println(mkString)
+
+ /**
+ * Renders this table to file.
+ *
+ * @param path File path.
+ */
+ def render(path: String): Unit = renderPrintStream(new PrintStream(path),
path)
+
+ /**
+ * Renders this table to file.
+ *
+ * @param file File.
+ */
+ def render(file: java.io.File): Unit = renderPrintStream(new
PrintStream(file), file.getAbsolutePath)
+
+ private def renderPrintStream(f: => PrintStream, file: String): Unit =
+ try
+ Using.resource(f) { ps =>
+ ps.print(mkString)
+ }
+ catch
+ case e: IOException => throw new NCException(s"Error outputting
table into file: $file", e)
+
+/**
+ * Static context.
+ */
+object NCAsciiTable:
+ // Default styles.
+ private final val DFLT_ROW_STYLE = "align:left"
+ private final val DFLT_HEADER_STYLE = "align:center"
+
+ /**
+ * Creates new ASCII text table with all defaults.
+ *
+ * @return Newly created ASCII table.
+ */
+ def apply() = new NCAsciiTable
+
+ /**
+ * Creates new ASCII table with given header cells.
+ *
+ * @param hdrs Header.
+ * @return Newly created ASCII table.
+ */
+ def apply(hdrs: Any*): NCAsciiTable = (new NCAsciiTable).#=(hdrs: _*)
+
+ /**
+ * Creates new ASCII table with given header cells.
+ *
+ * @param hdrs Header.
+ * @return Newly created ASCII table.
+ */
+ def apply(hdrs: mutable.Seq[_]): NCAsciiTable = (new
NCAsciiTable).#=(hdrs.toSeq: _*)
+
+ /**
+ * Creates new ASCII table with given headers and data.
+ *
+ * @param hdrs Headers.
+ * @param data Table data (sequence of rows).
+ * @return Newly created ASCII table.
+ */
+ def of(hdrs: Seq[Any], data: Seq[Seq[Any]]): NCAsciiTable =
+ val tbl = (new NCAsciiTable).#=(hdrs: _*)
+ data.foreach(tbl.+=(_: _*))
+ tbl
+
diff --git
a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
index eff80c5..330cc94 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
@@ -21,6 +21,8 @@ import com.typesafe.scalalogging.LazyLogging
import org.apache.nlpcraft.common.ansi.NCAnsi.*
import java.util.Random
+import java.util.regex.Pattern
+import scala.annotation.tailrec
import scala.sys.SystemProperties
/**
@@ -30,6 +32,7 @@ object NCUtils extends LazyLogging:
final val NL = System getProperty "line.separator"
private val RND = new Random()
private val sysProps = new SystemProperties
+ private final val ANSI_SEQ = Pattern.compile("\u001B\\[[?;\\d]*[a-zA-Z]")
private val ANSI_FG_8BIT_COLORS = for (i <- 16 to 255) yield ansi256Fg(i)
private val ANSI_BG_8BIT_COLORS = for (i <- 16 to 255) yield ansi256Bg(i)
private val ANSI_FG_4BIT_COLORS = Seq(
@@ -279,3 +282,132 @@ object NCUtils extends LazyLogging:
* Shortcut - current timestamp in milliseconds.
*/
def now(): Long = System.currentTimeMillis()
+
+ /**
+ *
+ * @param v
+ * @param dflt
+ * @tparam T
+ * @return
+ */
+ def notNull[T <: AnyRef](v: T, dflt: T): T = if (v == null) dflt else v
+
+ /**
+ * Strips ANSI escape sequences from the given string.
+ *
+ * @param s
+ * @return
+ */
+ def stripAnsi(s: String): String =
+ ANSI_SEQ.matcher(s).replaceAll("")
+
+ /**
+ * Trims each sequence string and filters out empty ones.
+ *
+ * @param s String to process.
+ * @return
+ */
+ def trimFilter(s: Seq[String]): Seq[String] =
+ s.map(_.strip).filter(_.nonEmpty)
+
+ /**
+ * Splits, trims and filters empty strings for the given string.
+ *
+ * @param s String to split.
+ * @param sep Separator (regex) to split by.
+ * @return
+ */
+ def splitTrimFilter(s: String, sep: String): Seq[String] =
+ trimFilter(s.split(sep).toIndexedSeq)
+
+ /**
+ * Recursively removes quotes from given string.
+ *
+ * @param s
+ * @return
+ */
+ @tailrec
+ def trimQuotes(s: String): String =
+ val z = s.strip
+ if (z.startsWith("'") && z.endsWith("'")) || (z.startsWith("\"") &&
z.endsWith("\"")) then
+ trimQuotes(z.substring(1, z.length - 1))
+ else
+ z
+
+ /**
+ * Recursively removes quotes and replaces escaped quotes from given
string.
+ *
+ * @param s
+ * @return
+ */
+ @tailrec
+ def trimEscapesQuotes(s: String): String =
+ val z = s.strip
+ if z.nonEmpty then
+ if z.head == '\'' && z.last == '\'' then
+ trimEscapesQuotes(z.substring(1, z.length - 1).replace("\'",
"'"))
+ else if z.head == '"' && z.last == '"' then
+ trimEscapesQuotes(z.substring(1, z.length - 1).replace("\\\"",
"\""))
+ else
+ z
+ else
+ z
+
+ /**
+ * Recursively removes quotes and replaces escaped quotes from given
string.
+ *
+ * @param s
+ * @return
+ */
+ @tailrec
+ def escapesQuotes(s: String): String =
+ if s.nonEmpty then
+ if s.head == '\'' && s.last == '\'' then
+ escapesQuotes(s.substring(1, s.length - 1).replace("\'", "'"))
+ else if (s.head == '"' && s.last == '"')
+ escapesQuotes(s.substring(1, s.length - 1).replace("\\\"",
"\""))
+ else
+ s
+ else
+ s
+
+ /**
+ *
+ * @param s
+ * @param sep
+ * @return
+ */
+ def normalize(s: String, sep: String): String =
+ splitTrimFilter(s, sep).mkString(sep)
+
+ /**
+ * Escapes given string for JSON according to RFC 4627
http://www.ietf.org/rfc/rfc4627.txt.
+ *
+ * @param s String to escape.
+ * @return Escaped string.
+ */
+ def escapeJson(s: String): String =
+ val len = s.length
+ if len == 0 then
+ ""
+ else
+ val sb = new StringBuilder
+ for (ch <- s.toCharArray)
+ ch match
+ case '\\' | '"' => sb += '\\' += ch
+ case '/' => sb += '\\' += ch
+ case '\b' => sb ++= "\\b"
+ case '\t' => sb ++= "\\t"
+ case '\n' => sb ++= "\\n"
+ case '\f' => sb ++= "\\f"
+ case '\r' => sb ++= "\\r"
+ case _ =>
+ if ch < ' ' then
+ val t = "000" + Integer.toHexString(ch)
+ sb ++= "\\u" ++= t.substring(t.length - 4)
+
+ else
+ sb += ch
+
+ sb.toString()
+