grid-printer.scm 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. ;; TODO: remove this load path stuff
  2. (add-to-load-path (dirname (current-filename)))
  3. (define-module (grid-printer)
  4. #:version (0 2 0)
  5. #:export (<grid-config>
  6. make-grid-config))
  7. (use-modules (helpers string-helpers)
  8. (helpers list-helpers)
  9. (srfi srfi-9 gnu))
  10. ;; =======
  11. ;; HELPERS
  12. ;; =======
  13. (define-public (find-longest-string-length* lst)
  14. (find-longest* lst string-length))
  15. (define identity
  16. (λ (sth) sth))
  17. ;; ===
  18. ;; LIB
  19. ;; ===
  20. (define-immutable-record-type <grid-config>
  21. ;; define constructor
  22. (make-grid-config col-sep row-sep intersection empty
  23. col-pad row-pad
  24. pad-direction)
  25. ;; define predicate
  26. grid-config?
  27. ;; define accessors and functional setters
  28. (col-sep get-col-sep set-col-sep)
  29. (row-sep get-row-sep set-row-sep)
  30. (intersection get-intersection set-intersection)
  31. (empty get-empty set-empty)
  32. (col-pad get-col-pad set-col-pad)
  33. (row-pad get-row-pad set-row-pad)
  34. (pad-direction get-pad-direction set-pad-direction))
  35. (define-public default-grid-config
  36. (make-grid-config
  37. #|col-sep|#
  38. "|"
  39. #|row-sep|#
  40. "-"
  41. #|intersection|#
  42. "+"
  43. #|empty|#
  44. " "
  45. #|col-pad|#
  46. 1
  47. #|row-pad|#
  48. 1
  49. #|pad-direction|#
  50. 'left))
  51. (define-public print-segmented-line
  52. (lambda* (seg-count seg-no-borders-width seg-filling seg-border #:optional (port (current-output-port)))
  53. "Print a line of characters, which is divided into segments. The separators
  54. used to divide the line are given as arguments."
  55. (let loop ([segs-remaining seg-count])
  56. (cond [(> segs-remaining 0)
  57. (display seg-border port)
  58. (display (string-repeat seg-filling seg-no-borders-width) port)
  59. (loop (- segs-remaining 1))]
  60. [else (display seg-border port)
  61. (display "\n" port)]))))
  62. (define-public col-content-width
  63. (lambda (data-part-width grid-config)
  64. "Calculate the width of a column (or the column including all padding and
  65. other contained content) given a grid configuration and the width of the content
  66. in the column."
  67. ;; One padding unit could be multiple characters wide, if the string for
  68. ;; empty or the string for row separator consists of multiple characters.
  69. (define width-of-one-padding (max (string-length (get-empty grid-config))
  70. (string-length (get-row-sep grid-config))))
  71. (define padding-one-side (* (get-col-pad grid-config) width-of-one-padding))
  72. (define padding-total (* 2 padding-one-side))
  73. (+ padding-total data-part-width)))
  74. (define-public print-empty-line
  75. (lambda* (fields# data-part-width grid-config #:optional (port (current-output-port)))
  76. "Print a line of characters, which in terms of the grid's content is
  77. considered empty."
  78. (print-segmented-line fields#
  79. (col-content-width data-part-width grid-config)
  80. (get-empty grid-config)
  81. (get-col-sep grid-config)
  82. port)))
  83. (define-public print-content-line
  84. (lambda* (min-field-count field-contents data-part-width grid-config
  85. #:optional (port (current-output-port)))
  86. "Print a line of characters, which contains content of the grid."
  87. (cond
  88. [(null? field-contents)
  89. (print-empty-line min-field-count data-part-width grid-config port)]
  90. [else
  91. (let loop ([count min-field-count] [contents field-contents])
  92. (let ([col-content
  93. (string-join
  94. (list
  95. (string-repeat (get-empty grid-config)
  96. (get-col-pad grid-config))
  97. (string-padding (if (null? contents)
  98. (get-empty grid-config)
  99. (car contents))
  100. data-part-width
  101. (get-empty grid-config)
  102. #:padding-direction (get-pad-direction grid-config))
  103. (string-repeat (get-empty grid-config) (get-col-pad grid-config)))
  104. "")])
  105. (display (get-col-sep grid-config) port)
  106. (display col-content port))
  107. (if (> count 1)
  108. (loop (- count 1)
  109. (if (null? contents) '() (cdr contents)))
  110. (display (get-col-sep grid-config) port)))])
  111. (display "\n" port)))
  112. (define-public print-separating-line
  113. (lambda* (fields# data-part-width grid-config
  114. #:optional (port (current-output-port)))
  115. "Print a line of characters, which acts as separator between rows."
  116. (print-segmented-line fields#
  117. (col-content-width data-part-width grid-config)
  118. (get-row-sep grid-config)
  119. (get-intersection grid-config)
  120. port)))
  121. ;; alias for better readability
  122. (define-public output-padding-line print-empty-line)
  123. (define-public get-nth-cell-parts
  124. (lambda (cells cell-parts-ref)
  125. "Get the nth part of each cell in a given list of cells. "
  126. ;; When cells consist of lists of strings, those strings are supposed to be
  127. ;; printed on separate lines. When printing to a port, we have to print line
  128. ;; by line. In consequence we need to first print all first parts, the first
  129. ;; strings in those lists of strings, then the second parts and so on. For
  130. ;; that purpose of getting all nth parts of the cells in a single list
  131. ;; `get-nth-cell-parts` is defined here.
  132. (map (λ (cell)
  133. (list-ref cell cell-parts-ref))
  134. cells)))
  135. (define-public equalize-lines-count
  136. (lambda (cells fill-elem)
  137. "Fill cells with parts so that each cell has the same number of lines."
  138. (let ([desired-len (longest-sublist-length cells)])
  139. (map (λ (cell-parts)
  140. (stretch-list cell-parts desired-len fill-elem))
  141. cells))))
  142. ;; A grid row are one or more lines of text which are printed between separation
  143. ;; lines.
  144. (define-public print-grid-row
  145. (lambda* (row-data fields# data-part-width grid-config #:optional port)
  146. "Print a row of the grid."
  147. (define content-lines# (longest-sublist-length row-data))
  148. (define (iter-padding n)
  149. (cond [(> n 0)
  150. (output-padding-line fields#
  151. data-part-width
  152. grid-config
  153. port)
  154. (iter-padding (- n 1))]
  155. [else (display "" port)]))
  156. (define (iter-content cells)
  157. (let loop ([cell-parts-ref 0])
  158. (cond
  159. [(= cell-parts-ref content-lines#)
  160. (display "" port)]
  161. [else
  162. (let ([nth-cell-parts (get-nth-cell-parts cells cell-parts-ref)])
  163. (print-content-line fields#
  164. nth-cell-parts
  165. data-part-width
  166. grid-config port))
  167. (loop (+ cell-parts-ref 1))])))
  168. ;; Print the padding above the content.
  169. (iter-padding (get-row-pad grid-config))
  170. ;; Print the content in possibly arbitrary number of lines,
  171. ;; depending on cell-value-split-proc.
  172. (iter-content (equalize-lines-count row-data (get-empty grid-config)))
  173. ;; Print the padding below the content.
  174. (iter-padding (get-row-pad grid-config))))
  175. (define-public print-grid
  176. (lambda* (data
  177. #:optional (port (current-output-port))
  178. #:key (grid-config default-grid-config))
  179. "Print a grid."
  180. "The argument data is expected to be a list of rows, of which each is a list
  181. of cells of which each is a list of strings or a simple string."
  182. "print-grid requires the input to be 2-dimensional or 3-dimensional."
  183. "print-grid requires the input to be nested list of strings."
  184. (define fields# (longest-sublist-length data))
  185. (define longest-string-length (find-longest-string-length* data))
  186. (define (iter data)
  187. (cond
  188. [(null? data) (display "" port)]
  189. [else
  190. ;; print initial separating line, outer top border
  191. (print-separating-line fields# longest-string-length grid-config port)
  192. ;; Print one row of content. This could result in multiple
  193. ;; content containing lines, depending on what
  194. ;; cell-value-split-proc does.
  195. (print-grid-row (car data) fields# longest-string-length grid-config port)
  196. ;; Continue with the next data point.
  197. (iter (cdr data))]))
  198. (cond
  199. [(member (dimendionality data) '(2 3))
  200. (let ([fields# (longest-sublist-length data)])
  201. (iter data)
  202. ;; final separating line, outer bottom border
  203. (print-separating-line fields# longest-string-length grid-config port))]
  204. [else
  205. (error "data dimendionality is not 1 or 2")])))