// // Block.swift // Blocks World created by stacking blocks one over/beside the other // and centering the smaller when lengths differ // Blocks are never modified, stacking creates new blocks // // Created by Guy Lapalme on 2023-07-27. // import Foundation struct Block:CustomStringConvertible { var content:[String] // the "real" init (private) that does the content creation private init(content:[String],_ border:String=""){ if border.count==0 { self.content = content } else { let w = content.count==0 ? 0 : content[0].count let line:[String] = border.count==0 ? [] : [String(repeating: border, count: w+2)] self.content = [line,content.map {border+$0+border},line].joined().map{$0} } } // create an empty block (might be useful!) init(){ self.init(content:[String]()) } // create a block from lines of text (might force a width and height) // if the text has more than one line then center each line // if specified, only the first char of the border string is used // height will be ignored if it is smaller than the number of lines in the text init(_ text:String,_ border:String="", width:Int?=nil, height:Int?=nil){ let lines = text.split(separator: "\n") var newText:[String] let newWidth:Int let newBorder = border.count>1 ? String(border[border.startIndex]) : border switch lines.count { case 0: // empty text newWidth = 0 newText = [""] case 1: // single line newWidth = text.count newText = [text] default: // many lines newWidth = lines.map {$0.count}.max()! newText = lines.map{Block.center(String($0),newWidth)} } if let width = width { // adjust width if width > newWidth { newText = newText.map {Block.center($0, width)} } } if let height = height { // adjust height newText = Block.center(vert: newText, height: height) } self.init(content:newText,newBorder) } // create a block from another one init(block:Block,_ border:String=""){ self.init(content: block.content,border) } var description:String { content.joined(separator: "\n") } // add block to the right of this one, centering the smaller on the left or the right func add(right:Block,_ border:String="")->Block { // ensure the block have the same height by centering the smaller with the respect to the larger let rightContent = Block.center(vert:right.content, height: content.count) let leftContent = Block.center(vert: content, height: right.content.count) // combine left and right return Block(content: zip(leftContent,rightContent).map {$0+$1},border) } // add a block on top of this one, centering the one with the smaller width over the other func add(bottom:Block,_ border:String="")->Block { let newWidth:Int = max(width,bottom.width) return Block(content:[content.map {Block.center($0,newWidth)}, bottom.content.map {Block.center($0,newWidth)}].joined().map{$0}, border) } func print(){ Swift.print(content.joined(separator: "\n")) } // useful tools built from the above... func add(right:[Block],_ border:String="")-> Block { return right.reduce(self,{bs,b in bs.add(right:b)}).border(border) } func add(left:Block,_ border:String="") -> Block { return left.add(right:self,border) } func add(left:[Block],_ border:String="")-> Block { return left.reduce(self,{bs,b in bs.add(left:b)}).border(border) } func add(bottom:[Block],_ border:String="")->Block { return bottom.reduce(self,{bs,b in bs.add(bottom:b)}).border(border) } func add(top:Block,_ border:String="")->Block{ return top.add(bottom: self,border) } func add(top:[Block],_ border:String="")->Block { return top.reduce(self,{bs,b in bs.add(top:b)}).border(border) } func repeating(horizontal:Int,_ border:String="") -> Block { return self.add(right:[Block](repeating: self, count:horizontal-1)).border(border) } func repeating(vertical:Int,_ border:String="") -> Block { return self.add(top:[Block](repeating: self, count:vertical-1)).border(border) } func border(_ b:String)->Block { b.count == 0 ? self : Block(block: self,b) } // private methods for implementation private var width:Int { get {content.count==0 ? 0 : content[0].count } } // return lengths of prefix and postfix when centering small within large private static func center(small:Int,large:Int)->(Int,Int){ assert(small<=large,"small (\(small)) > large (\(large))") let before = (large-small)/2 let after = large - small - before return (before,after) } // return centered text into width by adding spaces before and after private static func center(_ text:String,_ width:Int) -> String { if text.count >= width {return text} let (prefix,suffix) = Block.center(small:text.count,large:width) return String(repeating: " ", count: prefix)+text+String(repeating: " ", count: suffix) } // return centered list of lines into height by adding blank lines before and after private static func center(vert:[String],height:Int) -> [String] { if vert.count >= height {return vert} let (prefix,suffix) = Block.center(small: vert.count, large: height) let emptyLine = vert.count == 0 ? "" : String(repeating: " ", count: vert[0].count) return [[String](repeating: emptyLine, count: prefix), vert, [String](repeating: emptyLine, count: suffix)].joined().map{$0} } } @resultBuilder struct BlocksBuilder { static func buildBlock(_ components: Block...) -> [Block] { components } // deal also with variadic parameter of list of blocks static func buildBlock(_ components: [Block]...) -> [Block] { Array(components.joined()) } // transform a single block into a list of blocks static func buildExpression(_ expression: Block) -> [Block] { [expression] } static func buildEither(first component: [Block]) -> [Block] { component } static func buildEither(second component: [Block]) -> [Block] { component } static func buildArray(_ components: [[Block]]) -> [Block] { Array(components.joined()) } } func Horiz(_ border:String="",@BlocksBuilder content:() -> [Block])->Block { let blocks = content() return blocks[0].add(right:Array(blocks[1...])).border(border) } func Vert(_ border:String="",@BlocksBuilder content:() -> [Block]) -> Block { let blocks = content() return blocks[0].add(bottom:Array(blocks[1...])).border(border) }