...

Source file src/golang.conradwood.net/go-easyops/utils/table.go

Documentation: golang.conradwood.net/go-easyops/utils

     1  package utils
     2  
     3  // a "table", like a spreadsheet that may be rendered on screen or so
     4  
     5  import (
     6  	"fmt"
     7  	"strings"
     8  )
     9  
    10  type Table struct {
    11  	addingRow int // row we're currently "writing" to (0...n)
    12  	rows      []*Row
    13  	headerRow *Row
    14  	hidden    map[int]bool
    15  	maxlenCol map[int]int // for each column(0..n) there _might_ be a maximum length set
    16  }
    17  
    18  type Row struct {
    19  	t     *Table
    20  	cells []*Cell
    21  }
    22  
    23  type Cell struct {
    24  	typ  int // 0=empty,1=string,2=uint64,3=timestamp,4=float64,5=bool,6=int64
    25  	txt  string
    26  	num  uint64
    27  	ts   uint32
    28  	f    float64
    29  	b    bool
    30  	snum int64
    31  }
    32  
    33  func (c *Cell) String() string {
    34  	if c.typ == 0 {
    35  		return ""
    36  	} else if c.typ == 1 {
    37  		return c.txt
    38  	} else if c.typ == 2 {
    39  		return fmt.Sprintf("%d", c.num)
    40  	} else if c.typ == 3 {
    41  		return TimestampString(c.ts)
    42  	} else if c.typ == 7 {
    43  		return TimestampAgeString(c.ts) + " (" + TimestampString(c.ts) + ")"
    44  	} else if c.typ == 4 {
    45  		return fmt.Sprintf("%0.2f", c.f)
    46  	} else if c.typ == 5 {
    47  		return fmt.Sprintf("%v", c.b)
    48  	} else if c.typ == 6 {
    49  		return fmt.Sprintf("%d", c.snum)
    50  	}
    51  	return fmt.Sprintf("type %d", c.typ)
    52  }
    53  
    54  // create a new row (writing will commence at a new row
    55  func (t *Table) NewRow() {
    56  	t.addingRow++
    57  }
    58  func (t *Table) getHeaderRow() *Row {
    59  	if t.headerRow == nil {
    60  		t.headerRow = &Row{t: t}
    61  	}
    62  	return t.headerRow
    63  }
    64  func (t *Table) AddHeader(s string) {
    65  	t.getHeaderRow().AddCell(&Cell{typ: 1, txt: s})
    66  }
    67  func (t *Table) AddHeaders(s ...string) {
    68  	for _, a := range s {
    69  		t.getHeaderRow().AddCell(&Cell{typ: 1, txt: a})
    70  	}
    71  }
    72  
    73  func (t *Table) GetRowOrCreate(num int) *Row {
    74  	for len(t.rows) <= num {
    75  		t.rows = append(t.rows, &Row{t: t})
    76  	}
    77  	return t.rows[num]
    78  }
    79  func (t *Table) AddBool(b bool) *Table {
    80  	r := t.GetRowOrCreate(t.addingRow)
    81  	r.AddCell(&Cell{typ: 5, b: b})
    82  	return t
    83  }
    84  func (t *Table) AddString(s string) *Table {
    85  	r := t.GetRowOrCreate(t.addingRow)
    86  	r.AddCell(&Cell{typ: 1, txt: s})
    87  	return t
    88  }
    89  func (t *Table) AddStrings(sts ...string) *Table {
    90  	for _, s := range sts {
    91  		r := t.GetRowOrCreate(t.addingRow)
    92  		r.AddCell(&Cell{typ: 1, txt: s})
    93  	}
    94  	return t
    95  }
    96  func (t *Table) AddTimestamp(ts uint32) *Table {
    97  	r := t.GetRowOrCreate(t.addingRow)
    98  	r.AddCell(&Cell{typ: 3, ts: ts})
    99  	return t
   100  }
   101  func (t *Table) AddTimestampWithAge(ts uint32) *Table {
   102  	r := t.GetRowOrCreate(t.addingRow)
   103  	r.AddCell(&Cell{typ: 7, ts: ts})
   104  	return t
   105  }
   106  func (t *Table) AddFloat64(f float64) *Table {
   107  	r := t.GetRowOrCreate(t.addingRow)
   108  	r.AddCell(&Cell{typ: 4, f: f})
   109  	return t
   110  }
   111  func (t *Table) AddUint32(i uint32) *Table {
   112  	t.AddUint64(uint64(i))
   113  	return t
   114  }
   115  func (t *Table) AddInt(i int) *Table {
   116  	t.AddUint64(uint64(i))
   117  	return t
   118  }
   119  func (t *Table) AddInt64(i int64) *Table {
   120  	r := t.GetRowOrCreate(t.addingRow)
   121  	r.AddCell(&Cell{typ: 6, snum: i})
   122  	return t
   123  }
   124  func (t *Table) AddUint64(i uint64) *Table {
   125  	r := t.GetRowOrCreate(t.addingRow)
   126  	r.AddCell(&Cell{typ: 2, num: i})
   127  	return t
   128  
   129  }
   130  func (r *Row) AddCell(cell *Cell) {
   131  	r.cells = append(r.cells, cell)
   132  }
   133  
   134  // return # of cells (considering the col<->idx mapping)
   135  func (r *Row) Cols() int {
   136  	return len(r.Cells())
   137  }
   138  
   139  // return all cells (considering the col<->idx mapping)
   140  func (r *Row) Cells() []*Cell {
   141  	if r.t.hidden == nil {
   142  		r.t.hidden = make(map[int]bool)
   143  	}
   144  	var res []*Cell
   145  	for i := 0; i < len(r.cells); i++ {
   146  		if r.t.hidden[i] {
   147  			continue
   148  		}
   149  		res = append(res, r.cells[i])
   150  	}
   151  	return res
   152  }
   153  
   154  // return a cell (considering the col<->idx mapping)
   155  func (r *Row) GetCell(idx int) *Cell {
   156  	col := r.t.idx2col(idx)
   157  	//	fmt.Printf("Want cell %d, using %d\n", idx, col)
   158  	if len(r.cells) <= col {
   159  		return nil
   160  	}
   161  	return r.cells[col]
   162  }
   163  
   164  func (t *Table) ToCSV() string {
   165  	rows := len(t.rows)
   166  	sb := strings.Builder{}
   167  	for i := 0; i < rows; i++ {
   168  		row := t.GetRowOrCreate(i)
   169  		if row.Cols() == 0 {
   170  			continue
   171  		}
   172  		line := ""
   173  		deli := ""
   174  		for cn := 0; cn < row.Cols(); cn++ {
   175  			cel := row.GetCell(cn)
   176  			s := escapeCell(cel.String())
   177  			line = line + deli + s
   178  			deli = ","
   179  		}
   180  		sb.WriteString(line + "\n")
   181  	}
   182  	return sb.String()
   183  }
   184  func escapeCell(s string) string {
   185  	s = strings.ReplaceAll(s, ",", "\\,")
   186  	return s
   187  }
   188  
   189  // column 0..n
   190  func (t *Table) DisableColumn(col int) {
   191  	if t.hidden == nil {
   192  		t.hidden = make(map[int]bool)
   193  	}
   194  	t.hidden[col] = true
   195  }
   196  
   197  // column 0..n
   198  func (t *Table) EnableColumn(col int) {
   199  	if t.hidden == nil {
   200  		return
   201  	}
   202  	t.hidden[col] = false
   203  }
   204  
   205  // column 0..n
   206  func (t *Table) EnableAllColumns() {
   207  	t.hidden = nil
   208  }
   209  
   210  // calculates the column offset (considering hidden columns)
   211  func (t *Table) idx2col(idx int) int {
   212  	if t.hidden == nil {
   213  		return idx
   214  	}
   215  	off := 0
   216  	for i := 0; i < idx; i++ {
   217  		if t.hidden[idx] {
   218  			off++
   219  		}
   220  	}
   221  	return idx + off
   222  }
   223  func (t *Table) GetMaxLen(col int) int {
   224  	if t.maxlenCol == nil {
   225  		t.maxlenCol = make(map[int]int)
   226  	}
   227  	f, found := t.maxlenCol[col]
   228  	if !found {
   229  		return 0xFFFFFFFF
   230  	}
   231  	return f
   232  
   233  }
   234  func (t *Table) SetMaxLen(col, width int) {
   235  	if t.maxlenCol == nil {
   236  		t.maxlenCol = make(map[int]int)
   237  	}
   238  	t.maxlenCol[col] = width
   239  }
   240  
   241  // gets "printing" rows. Multi-line text or text that is wrapped will create an extra row
   242  func (t *Table) GetPrintingRows() []*Row {
   243  	var res []*Row
   244  	for _, r := range t.rows {
   245  		res = append(res, r.toPrintingRows()...)
   246  	}
   247  	return res
   248  }
   249  
   250  // wrap text and return multiple rows
   251  func (r *Row) toPrintingRows() []*Row {
   252  	var res []*Row
   253  	for cn, c := range r.Cells() {
   254  		if c.typ != 1 {
   255  			// it's not a string, insert cell as is to row
   256  			if len(res) == 0 {
   257  				res = append(res, &Row{t: r.t})
   258  			}
   259  			r := res[0]
   260  			for len(r.cells) <= cn {
   261  				r.cells = append(r.cells, &Cell{})
   262  			}
   263  			r.cells[cn] = c
   264  			continue
   265  		}
   266  
   267  		// it's a string.. wrap text
   268  		ml := r.t.GetMaxLen(cn)
   269  		s := c.String()
   270  		lines := strings.Split(s, "\n")
   271  		var tlines []string
   272  		for _, l := range lines {
   273  			for {
   274  				if len(l) <= ml {
   275  					tlines = append(tlines, l)
   276  					break
   277  				}
   278  				splitAt := findBetterSplitAt(l, ml)
   279  				nl := l[:splitAt]
   280  				tlines = append(tlines, nl)
   281  				l = l[splitAt:]
   282  			}
   283  		}
   284  		for len(res) < len(tlines) {
   285  			res = append(res, &Row{t: r.t})
   286  		}
   287  		for i, l := range tlines {
   288  			r := res[i]
   289  			for len(r.cells) <= cn {
   290  				r.cells = append(r.cells, &Cell{})
   291  			}
   292  			r.cells[cn] = &Cell{typ: c.typ, txt: l}
   293  
   294  		}
   295  	}
   296  	return res
   297  }
   298  func findBetterSplitAt(line string, proposed int) int {
   299  	splits := []byte{' ', ':'}
   300  	i := proposed
   301  	j := 0
   302  	for {
   303  		j++
   304  		if j > 20 {
   305  			return proposed
   306  		}
   307  		i--
   308  		if i < 0 {
   309  			return proposed
   310  		}
   311  		for _, sp := range splits {
   312  			if line[i] == sp {
   313  				if i < proposed {
   314  					return i + 1
   315  				}
   316  				return i
   317  			}
   318  		}
   319  
   320  	}
   321  
   322  }
   323  

View as plain text