Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

The version below is part of a larger project that you are free to grab and use for you own. The main project wiki page is
  here. We prefer static typing for our larger projects, so we have modified the original code to suit our style. Hope it is useful for you. Victor Lasenko Vlasenko authored most of this enhancement under contract for Build Lackey Labs, and I helped clean up the code a bit for presentation on this site Chris/Lead Lackey at the Labs[].

Summary of Enhancement

 This enhancement enables wrapping-to-multi-lines for  column text
that exceeds the width of a given column. We break out word boundaries
(white space) where possible,  The implementation we built on truncated
the overly long column value instead of wrapping it.

Example:

Code Block
    for this data
                 [col1: '1',
                 col2: 'A very long text messsage for you with a superLongWordThatCantBeBrokenAtAWordBoundaryWithoutwrapping']

            Instead of this:


                    col1           col2
                    ----      --------------------
                     1         A very long text mes

            We get this:

                    col1           col2
                    ----      --------------------
                     1         A very long text
                               message for you with
                               a superLongWordThatC
                              antBeBrokenOnWordBou
                              ndaryWithoutwrapping



Code Block
package com.lackey.provis.cmdline.util

/*
 *  Reproduced from
 * 	http://groovy.codehaus.org/Formatting+simple+tabular+text+data
 * (with some code reformatting according to my readability preferences)
 */


class TableTemplateFactory {
  def columns = [];     // contains columns names and their length
  def columnLen = [:];  // contains lengthes of the columns
  def header1 = '';     // contains columns names
  def header2 = '';     // contains underscores
  def body = '';        // the rows of the table
  def footer = '';      // actually unused: can contain footer notes, totals, etc.

  /**
   * Breaks up long line into multiline at the word boundary
   *
   * TODO move this method to some generic text utils class
   *
   * @param input long input line
   * @param lineWidth maximum output lines width
   *
   * @return multiline as an array of strings
   */
  protected static List<String> wrapLine(input, lineWidth) {
    List<String>  lines = []
    def           line = ""
    def           addWord;

    addWord = {word ->
      // Add new word if we have space in current line
      if ((line.size() + word.size()) <= lineWidth) {
        line <<= word
        if (line.size() < lineWidth)
          line <<= " "
        // Our word is longer than line width, break it up
      } else if (word.size() > lineWidth) {
        def len = lineWidth - line.length()
        line <<= word.substring(0, len)
        word = word.substring(len)
        lines += line.toString()

        while (word.size() > lineWidth) {
          lines += word.substring(0, lineWidth);
          word = word.substring(lineWidth);
        }
        line = word
        if (line.size() > 0 && line.size() < lineWidth)
          line <<= " "
        // No more space in line - wrap to another line
      } else {
        lines += line.toString()
        line = ""

        addWord(word)
      }
    }

    input.split(" ").each() {
      addWord(it)
    }

    lines += line.toString()

    return lines
  }

  /**
   * Wraps values in rows according to the column width. Value wrapping performed at
   * the word boundary.
   *
   * @param rows input rows array
   *
   * @return rows array with multiline values
   */
  public List<Map<String,String>>  wrapRows(unwrappedRows) {
    List<Map<String,List<String>>> multilineRows = []
    List<Integer> rowHeights = []

    // Preprare unwrappedRows with multiline values
    unwrappedRows.each() {
      unwrappedRow ->
      def multiLineRow = [:]
      int height = 1
      unwrappedRow.each() {
        column   ->  // column in unwrapped row
        List<String> multilineValue = wrapLine(column.value, columnLen[column.key])
        if (multilineValue.size() > height)
          height = multilineValue.size()
        multiLineRow[column.key] = multilineValue   // multiLineValue  is list of strings
      }
      multilineRows << multiLineRow
      rowHeights << height
    }

    return foldMultiLineRowsIntoPlainRows(multilineRows, rowHeights)
  }


  // For each array of strings (wrapped lines) in multiLineRows we fold those in to
  // a new array of rows (plain rows). For any given columnn that consists of an array
  // of wrapped lines we either insert the appropriate wrapped line, or a blank if no
  // more lines are left for that column.
  //
  private List<Map<String,String>>  foldMultiLineRowsIntoPlainRows(
                                                   List<Map<String,List<String>>>  multilineRows,
                                                   List<Integer> rowHeights) {
    List<Map<String,String>> plainRows = []
    multilineRows.eachWithIndex() {
      Map<String,List<String>> mlRow, int idx ->
      int height = rowHeights[idx]

      for (i in 0..<height) {
        Map<String,String> row = [:]

        mlRow.each() {
          Map.Entry<String, List<String>> col ->
          List<String> listOfStringsForColumn = mlRow[col.key]
          row[col.key] = listOfStringsForColumn[i] ?: ""
        }
        plainRows << row
      }
    }

    return plainRows
  }

  public TableTemplateFactory addColumn(String name, int size) {
    columns << [name: name, size: size];
    columnLen[name] = size
    return this
  }

  def getTemplate() {
    header1 = "\n";
    columns.each {
      header1 += ' <%print "' + it.name + '".center(' + it.size + ')%> '
    };
    header2 = "\n";
    columns.each {
      header2 += ' <%print "_"*' + it.size + ' %> '
    };
    body = '\n<% rows.each {%>';

    // If a value is longer than given column name, it will be trunked
    columns.each {
      body +=
        ' ${it.' +
                it.name + '.toString().padRight(' +
                it.size + ').substring(0,' +
                it.size +
                ')} '
    };
    body += '\n<% } %>';
    return header1 + header2 + body + footer;
  }
}


package com.lackey.provis;
/*
 *
 * 	Copyright 2009. Build Lackey Labs. 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.
 *
 * Author: cbedford
 * Date: Jan 18, 2009
 * Time: 3:21:52 PM
 */


import org.testng.annotations.*
import org.testng.TestNG
import org.testng.TestListenerAdapter

import org.slf4j.*
import groovy.text.Template;
import groovy.text.SimpleTemplateEngine
import com.lackey.provis.cmdline.util.TableTemplateFactory


public class TestTableTemplates {
  private static Logger logger = LoggerFactory.getLogger(TestTableTemplates.class.name)

  @Test (enabled = true)
  public void test_X() {
    final TableTemplateFactory factory = new TableTemplateFactory()
    def ttf = factory.addColumn("name", 15).addColumn("age", 4)
    def names = [] << [name:"Raffaele Smoke BOON laforge calm nwe box ooex", age:"23"] << [name:"Griorgio", age:"30"]
    def binding = ['rows': names]
    println new SimpleTemplateEngine().createTemplate(ttf.template).make(binding).toString()
  }

  @Test (enabled = true)
  public void testWrapLineSingleWord() {
    TableTemplateFactory ttf = new TableTemplateFactory()
    assert (ttf.wrapLine("aa bb cc", 2) == ["aa", "bb", "cc"])
  }

  @Test (enabled = true)
  public void testWrapLineSeveralWords() {
    TableTemplateFactory ttf = new TableTemplateFactory()
    assert (ttf.wrapLine("This is some text that must be broken up to multiline" +
        " at a word boundary", 9) == ["This is ", "some text", "that must", "be broken",
                                       "up to ", "multiline", "at a word", "boundary "])
  }

  @Test (enabled = true)
  public void testWrapLineWithLongWord() {
    TableTemplateFactory ttf = new TableTemplateFactory()
    assert (ttf.wrapLine("A very long text message for you with a" +
        " superLongWordThatCantBeBrokenOnWordBoundaryWithoutwrapping", 20) ==
          ["A very long text ", "message for you with", "a superLongWordThatC",
           "antBeBrokenOnWordBou", "ndaryWithoutwrapping"])
  }

  @Test (enabled = true)
  public void testMultilineRowWrappingWithActualPrinting() {
    def ttf = new TableTemplateFactory().addColumn("name", 15).addColumn("age", 4)
    def names = [] << [name:"Raffaele Gudinio BanderasHulioLopezMakachino", age:"819 4096"] << [name:"Griorgio", age:"30"]
    def wrappedNames = ttf.wrapRows(names)
    assert wrappedNames ==
            [["name":"Raffaele ",       "age":"819 "],
                    ["name":"Gudinio Bandera", "age":"4096"],
                    ["name":"sHulioLopezMaka", "age":""],
                    ["name":"chino ",          "age":""],
                    ["name":"Griorgio ",       "age":"30 "]]

    def binding = ['rows':   wrappedNames ]
    final String templateOutput =  getTemplateOutput(binding, ttf)
    System.out.println templateOutput

  }

  private String getTemplateOutput(Map<Object, List> binding, TableTemplateFactory ttf) {
    return new SimpleTemplateEngine().createTemplate(ttf.template).make(binding).toString()
  }
}

...