All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gridgain.scalar.ScalarTextTable.scala Maven / Gradle / Ivy

/* 
 Copyright (C) GridGain Systems. All Rights Reserved.
 
 Licensed 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.gridgain.scalar

import collection.immutable._
import org.gridgain.grid.util.typedef.internal._

/**
 * ==Overview==
 * Provides `ASCII`-based table with minimal styling support.
 */
class ScalarTextTable {
    /**
     * Cell style.
     */
    private sealed case class Style(
        var leftPad: Int = 1, // >= 0
        var rightPad: Int = 1, // >= 0
        var align: String = "center" // center, left, right
    ) {
        assert(leftPad >= 0)
        assert(rightPad >= 0)
        assert(align != null)

        /**
         * Gets overall padding (left + right).
         */
        def padding: Int =
            leftPad + rightPad
    }

    /**
     * Cell style.
     */
    private object Style {
        /**
         *
         * @param sty Style.
         */
        def apply(sty: String): Style = {
            assert(sty != null)

            val cs = new Style

            if (!sty.isEmpty) {
                for (e <- sty.split(',')) {
                    val a = e.split(":")

                    assert(a.length == 2, "Invalid cell style: " + e.trim)

                    a(0).trim() match {
                        case "leftPad" => cs.leftPad = a(1).trim.toInt
                        case "rightPad" => cs.rightPad = a(1).trim.toInt
                        case "align" => cs.align = a(1).trim
                        case _ => assert(false, "Invalid style: " + e.trim)
                    }
                }
            }

            cs
        }
    }

    /**
     * Cell holder.
     */
    private sealed case class Cell(style: Style, lines: Seq[String]) {
        assert(style != null)
        assert(lines != null)

        /**
         * Cell's calculated width including padding.
         */
        lazy val width =
            if (height > 0)
                style.padding + lines.max(Ordering.by[String, Int](_.length)).length
            else
                style.padding

        /**
         * Gets height of the cell.
         */
        def height: Int =
            lines.length
    }

    /**
     * Margin holder.
     */
    private sealed case class Margin(
        top: Int = 0,
        right: Int = 0,
        bottom: Int = 0,
        left: Int = 0) {
        assert(top >= 0)
        assert(right >= 0)
        assert(bottom >= 0)
        assert(left >= 0)
    }

    /** */
    private val HDR_HOR: Char = '='

    /** */
    private val HDR_VER: Char = '|'

    /** */
    private val HDR_CRS: Char = '+'

    /** */
    private val ROW_HOR: Char = '-'

    /** */
    private val ROW_VER: Char = '|'

    /** */
    private val ROW_CRS: Char = '+'

    /** */
    private val NL = '\n'

    /** Headers. */
    private var hdr = List.empty[Cell]

    /** Rows. */
    private var rows = List.empty[List[Cell]]

    /** Current row, if any. */
    private var curRow: List[Cell] = null

    /** Table's margin, if any. */
    private var margin: Margin = Margin()

    /** Default row cell style, if any. */
    private var rowSty: String = "align:left"

    /** Default header cell style, if any. */
    private var hdrSty: String = "align:center"

    /**
     * Flag indicating whether or not to draw inside horizontal lines
     * between individual rows.
     */
    var insideBorder = false

    /**
     * Flag indicating whether of not to automatically draw horizontal lines
     * for multiline rows.
     */
    var autoBorder = true

    /**
     * Maximum width of the cell. If any line in the cell exceeds this width
     * it will be cut in two or more lines.
     *
     * '''NOTE''': it doesn't include into account the padding. Only the actual
     * string length is counted.
     */
    var maxCellWidth = Int.MaxValue

    /**
     *
     * @param ch Character.
     * @param len Length of the dash.
     */
    private def dash(ch: Char, len: Int): String = {
        assert(len >= 0)

        new String().padTo(len, ch)
    }

    /**
     *
     * @param len Length.
     */
    private def blank(len: Int): String =
        dash(' ', 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) {
        assert(top >= 0)
        assert(right >= 0)
        assert(bottom >= 0)
        assert(left >= 0)

        margin = Margin(top, right, bottom, left)
    }

    /**
     * Starts data row.
     */
    def startRow() {
        assert(curRow == null)

        curRow = List.empty[Cell]
    }

    /**
     * Ends data row.
     */
    def endRow() {
        assert(!curRow.isEmpty)

        rows = rows :+ curRow

        curRow = null
    }

    /**
     * Adds row (one or more row cells).
     *
     * @param cells Row cells. For multi-line cells - use `Seq(...)`.
     */
    def +=(cells: Any*): ScalarTextTable = {
        startRow()

        cells foreach {
            case s: scala.collection.Iterable[Any] => addRowCell(s.toSeq: _*)
            case p: Product => addRowCell(p.productIterator.toSeq: _*)
            case a => addRowCell(a)
        }

        endRow()

        this
    }

    /**
     * Adds header (one or more header cells).
     *
     * @param cells Header cells. For multi-line cells - use `Seq(...)`.
     */
    def #=(cells: Any*): ScalarTextTable = {
        cells foreach {
            case s: scala.collection.Iterable[Any] => addHeaderCell(s.toSeq: _*)
            case p: Product => addHeaderCell(p.productIterator.toSeq: _*)
            case a => addHeaderCell(a)
        }

        this
    }

    /**
     * Adds single header cell.
     *
     * @param lines One or more cell lines.
     */
    def addHeaderCell(lines: Any*): ScalarTextTable = {
        assert(lines != null)
        assert(lines.length > 0)

        var lst = List.empty[String]

        // Break up long line into multiple ones - if necessary.
        lines foreach((line: Any) =>
            line.toString.grouped(maxCellWidth) foreach((grp: String) => lst = lst :+ grp))

        hdr = hdr :+ Cell(Style(hdrSty), lst)

        this
    }

    /**
     * Gets current row style.
     */
    def rowStyle =
        rowSty

    /**
     * Sets current row style.
     *
     * @param rowSty Row style to set.
     */
    def rowStyle(rowSty: String) {
        this.rowSty = rowSty
    }

    /**
     * Gets current header style.
     */
    def headerStyle =
        rowSty

    /**
     * Sets current header style.
     *
     * @param hdrSty Header style to set.
     */
    def headerStyle(hdrSty: String) {
        this.hdrSty = hdrSty
    }

    /**
     * Adds single row cell.
     *
     * @param lines One or more row cells. Multiple lines will be printed on separate lines.
     */
    def addRowCell(lines: Any*): ScalarTextTable = {
        assert(lines != null)
        assert(lines.length >= 0)
        assert(curRow != null)

        var lst = List.empty[String]

        // Break up long line into multiple ones - if necessary.
        lines foreach((line: Any) =>
            line.toString.grouped(maxCellWidth) foreach((grp: String) => lst = lst :+ grp))

        curRow = curRow :+ Cell(Style(rowSty), lst)

        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 = {
        assert(txt != null)
        assert(width > 0)
        assert(sty != null)
        assert(txt.length <= width)

        val d = width - txt.length

        sty.align.trim match {
            case "center" =>
                blank(d / 2) + txt + blank(d / 2 + d % 2)
            case "left" =>
                blank(sty.leftPad) + txt + blank(d - sty.leftPad)
            case "right" =>
                blank(d - sty.rightPad) + txt + blank(sty.rightPad)
            case _ =>
                throw new AssertionError("Invalid align option in: " + sty)
        }
    }

    /**
     * Renders this table.
     */
    def render() {
        // Make sure table is not empty.
        if (hdr.isEmpty && rows.isEmpty)
            return

        var colsNum = -1

        val isHdr = !hdr.isEmpty

        if (isHdr)
            colsNum = hdr.size

        // Calc number of columns and make sure all rows are even.
        for (r <- rows)
            if (colsNum == -1)
                colsNum = r.size
            else if (colsNum != r.size)
                assert (false, "Table with uneven rows.")

        assert(colsNum > 0)

        // At this point all rows in the table have the
        // the same number of columns.

        val colWs = new Array[Int](colsNum) // Column widths.
        val rowHs = new Array[Int](rows.length) // Row heights.

        // Header height.
        var hdrH = 0

        // Initialize column widths with header row (if any).
        for (i <- 0 until hdr.size) {
            val c = hdr(i)

            colWs(i) = c.width

            hdrH = math.max(hdrH, c.height)
        }

        // Calc row heights and column widths.
        for (i <- 0 until rows.length; j <- 0 until colsNum) {
            val c = rows(i)(j)

            rowHs(i) = math.max(rowHs(i), c.height)
            colWs(j) = math.max(colWs(j), c.width)
        }

        // Table width without the border.
        val tblW = colWs.sum + colsNum - 1

        val tbl = new SB()

        // Top margin.
        for (i <- 0 until margin.top)
            tbl.a(" ").a(NL)

        // Print header, if any.
        if (isHdr) {
            tbl.a(blank(margin.left)).a(HDR_CRS).a(dash(HDR_HOR, tblW)).a(HDR_CRS).a(blank(margin.right)).a(NL)

            for (i <- 0 until hdrH) {
                // Left margin and '|'.
                tbl.a(blank(margin.left)).a(HDR_VER)

                for (j <- 0 until hdr.size) {
                    val c = hdr(j)

                    if (i >= 0 && i < c.height)
                        tbl.a(aligned(c.lines(i), colWs(j), c.style))
                    else
                        tbl.a(blank(colWs(j)))

                    tbl.a(HDR_VER) // '|'
                }

                // Right margin.
                tbl.a(blank(margin.right)).a(NL)
            }

            tbl.a(blank(margin.left)).a(HDR_CRS).a(dash(HDR_HOR, tblW)).a(HDR_CRS).a(blank(margin.right)).a(NL)
        }
        else
            tbl.a(blank(margin.left)).a(ROW_CRS).a(dash(ROW_HOR, tblW)).a(ROW_CRS).a(blank(margin.right)).a(NL)

        // Print rows, if any.
        if (!rows.isEmpty) {
            val horLine = (i: Int) => {
                // Left margin and '+'
                tbl.a(blank(margin.left)).a(ROW_CRS)

                for (k <- 0 until rows(i).size)
                    tbl.a(dash(ROW_HOR, colWs(k))).a(ROW_CRS)

                // Right margin.
                tbl.a(blank(margin.right)).a(NL)
            }

            for (i <- 0 until rows.size) {
                val r = rows(i)

                val rowH = rowHs(i)

                if (i > 0 && ((rowH > 1 && autoBorder) || insideBorder) && rowHs(i - 1) == 1)
                    horLine(i)

                for (j <- 0 until rowH) {
                    // Left margin and '|'
                    tbl.a(blank(margin.left)).a(ROW_VER)

                    for (k <- 0 until r.size) {
                        val c = r(k)
                        val w = colWs(k)

                        if (j < c.height)
                            tbl.a(aligned(c.lines(j), w, c.style))
                        else
                            tbl.a(blank(w))

                        tbl.a(ROW_VER) // '|'
                    }

                    // Right margin.
                    tbl.a(blank(margin.right)).a(NL)
                }

                if (i < rows.size - 1 && ((rowH > 1 && autoBorder) || insideBorder))
                    horLine(i)
            }

            tbl.a(blank(margin.left)).a(ROW_CRS).a(dash(ROW_HOR, tblW)).a(ROW_CRS).a(blank(margin.right)).a(NL)
        }

        // Bottom margin.
        for (i <- 1 to margin.bottom)
            tbl.a(" ").a(NL)

        print(tbl.toString)
    }
}

/**
 * Static context.
 */
object ScalarTextTable {
    /**
      * Creates new Scalar text table.
      */
    def apply() =
        new ScalarTextTable
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy