├── .github └── workflows │ └── main.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── TextTable │ ├── FancyGridFormat.swift │ ├── GridFormat.swift │ ├── HtmlFormat.swift │ ├── LatexFormat.swift │ ├── OrgFormat.swift │ ├── PipeFormat.swift │ ├── PlainFormat.swift │ ├── PsqlFormat.swift │ ├── RstFormat.swift │ ├── SimpleFormat.swift │ ├── String+Util.swift │ └── TextTable.swift ├── TODO.md └── Tests └── TextTableTests ├── FancyGridFormatTests.swift ├── GridFormatTests.swift ├── HtmlFormatTests.swift ├── LatexFormatTests.swift ├── OrgFormatTests.swift ├── PipeFormatTests.swift ├── PlainFormatTests.swift ├── PsqlFormatTests.swift ├── RstFormatTests.swift ├── SimpleFormatTests.swift └── TestUtil.swift /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: macOS-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Build project 19 | run: swift build 20 | - name: Test project 21 | run: swift test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode 7 Source Projects 3 | # can be used for watchOS, tvOS, iOS, OSX, Swift development 4 | # 5 | # February 2016 6 | # 7 | # Save this file as .gitignore in your repository's working directly. 8 | # Note: Don't confuse the working directory with the .git directory. It will not work if you put it there. 9 | # 10 | ##### 11 | # OS X temporary files that should never be committed 12 | # 13 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 14 | 15 | .DS_Store 16 | 17 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 18 | 19 | .Trashes 20 | 21 | # c.f. http://www.westwind.com/reference/os-x/invisibles.html 22 | 23 | *.swp 24 | 25 | # 26 | # *.lock - this is used and abused by many editors for many different things. 27 | # For the main ones I use (e.g. Eclipse), it should be excluded 28 | # from source-control, but YMMV. 29 | # (lock files are usually local-only file-synchronization on the local FS that should NOT go in git) 30 | # c.f. the "OPTIONAL" section at bottom though, for tool-specific variations! 31 | # 32 | # In particular, if you're using CocoaPods, you'll want to comment-out this line: 33 | #*.lock 34 | 35 | 36 | # 37 | # profile - REMOVED temporarily (on double-checking, I can't find it in OS X docs?) 38 | #profile 39 | 40 | 41 | #### 42 | # Xcode temporary files that should never be committed 43 | # 44 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 45 | 46 | *~.nib 47 | 48 | 49 | #### 50 | # Xcode build files - 51 | # 52 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 53 | 54 | DerivedData/ 55 | 56 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 57 | 58 | build/ 59 | 60 | 61 | ##### 62 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 63 | # 64 | # This is complicated: 65 | # 66 | # SOMETIMES you need to put this file in version control. 67 | # Apple designed it poorly - if you use "custom executables", they are 68 | # saved in this file. 69 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 70 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 71 | 72 | # .pbxuser: http://lists.apple.com/archives/xcode-users/2004/Jan/msg00193.html 73 | 74 | *.pbxuser 75 | 76 | # .mode1v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html 77 | 78 | *.mode1v3 79 | 80 | # .mode2v3: http://lists.apple.com/archives/xcode-users/2007/Oct/msg00465.html 81 | 82 | *.mode2v3 83 | 84 | # .perspectivev3: http://stackoverflow.com/questions/5223297/xcode-projects-what-is-a-perspectivev3-file 85 | 86 | *.perspectivev3 87 | 88 | # NB: also, whitelist the default ones, some projects need to use these 89 | !default.pbxuser 90 | !default.mode1v3 91 | !default.mode2v3 92 | !default.perspectivev3 93 | 94 | 95 | #### 96 | # Xcode 4 - semi-personal settings 97 | # 98 | # Apple Shared data that Apple put in the wrong folder 99 | # c.f. http://stackoverflow.com/a/19260712/153422 100 | # FROM ANSWER: Apple says "don't ignore it" 101 | # FROM COMMENTS: Apple is wrong; Apple code is too buggy to trust; there are no known negative side-effects to ignoring Apple's unofficial advice and instead doing the thing that actively fixes bugs in Xcode 102 | # Up to you, but ... current advice: ignore it. 103 | *.xccheckout 104 | 105 | # 106 | # 107 | # OPTION 1: --------------------------------- 108 | # throw away ALL personal settings (including custom schemes! 109 | # - unless they are "shared") 110 | # As per build/ and DerivedData/, this ought to have a trailing slash 111 | # 112 | # NB: this is exclusive with OPTION 2 below 113 | xcuserdata/ 114 | 115 | # OPTION 2: --------------------------------- 116 | # get rid of ALL personal settings, but KEEP SOME OF THEM 117 | # - NB: you must manually uncomment the bits you want to keep 118 | # 119 | # NB: this *requires* git v1.8.2 or above; you may need to upgrade to latest OS X, 120 | # or manually install git over the top of the OS X version 121 | # NB: this is exclusive with OPTION 1 above 122 | # 123 | #xcuserdata/**/* 124 | 125 | # (requires option 2 above): Personal Schemes 126 | # 127 | #!xcuserdata/**/xcschemes/* 128 | 129 | #### 130 | # XCode 4 workspaces - more detailed 131 | # 132 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 133 | # 134 | # Workspace layout is quite spammy. For reference: 135 | # 136 | # /(root)/ 137 | # /(project-name).xcodeproj/ 138 | # project.pbxproj 139 | # /project.xcworkspace/ 140 | # contents.xcworkspacedata 141 | # /xcuserdata/ 142 | # /(your name)/xcuserdatad/ 143 | # UserInterfaceState.xcuserstate 144 | # /xcshareddata/ 145 | # /xcschemes/ 146 | # (shared scheme name).xcscheme 147 | # /xcuserdata/ 148 | # /(your name)/xcuserdatad/ 149 | # (private scheme).xcscheme 150 | # xcschememanagement.plist 151 | # 152 | # 153 | 154 | #### 155 | # Xcode 4 - Deprecated classes 156 | # 157 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 158 | # 159 | # We're using source-control, so this is a "feature" that we do not want! 160 | 161 | *.moved-aside 162 | 163 | #### 164 | # OPTIONAL: Some well-known tools that people use side-by-side with Xcode / iOS development 165 | # 166 | # NB: I'd rather not include these here, but gitignore's design is weak and doesn't allow 167 | # modular gitignore: you have to put EVERYTHING in one file. 168 | # 169 | # COCOAPODS: 170 | # 171 | # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#what-is-a-podfilelock 172 | # c.f. http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 173 | # 174 | #!Podfile.lock 175 | # 176 | # RUBY: 177 | # 178 | # c.f. http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/ 179 | # 180 | #!Gemfile.lock 181 | # 182 | # IDEA: 183 | # 184 | # c.f. https://www.jetbrains.com/objc/help/managing-projects-under-version-control.html?search=workspace.xml 185 | # 186 | #.idea/workspace.xml 187 | # 188 | # TEXTMATE: 189 | # 190 | # -- UNVERIFIED: c.f. http://stackoverflow.com/a/50283/153422 191 | # 192 | #tm_build_errors 193 | 194 | # Swift Package Manager 195 | # 196 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 197 | Packages/ 198 | .build/ 199 | 200 | # CocoaPods 201 | # 202 | # We recommend against adding the Pods directory to your .gitignore. However 203 | # you should judge for yourself, the pros and cons are mentioned at: 204 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 205 | # 206 | # Pods/ 207 | 208 | # Carthage 209 | # 210 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 211 | # Carthage/Checkouts 212 | 213 | Carthage/Build 214 | 215 | # fastlane 216 | # 217 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 218 | # screenshots whenever they are needed. 219 | # For more information about the recommended setup visit: 220 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 221 | 222 | fastlane/report.xml 223 | fastlane/screenshots 224 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | compiler: 8 | - clang 9 | 10 | os: 11 | - osx 12 | 13 | osx_image: xcode10.2 14 | 15 | script: 16 | - swift build 17 | - swift test 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.0-alpha.4](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.4) 4 | 5 | ### What's New 6 | 7 | * Update for Xcode 8 beta 6 (Apple Swift version 3.0 (swiftlang-800.0.43.6 clang-800.0.38)) 8 | * Travis-CI integration 9 | 10 | ### SPM Dependency Snippet 11 | 12 | ```Swift 13 | .Package(url: "https://github.com/cfilipov/TextTable", Version(1, 0, 0, prereleaseIdentifiers: ["alpha", "4"])) 14 | ``` 15 | 16 | ## [1.0.0-alpha.3](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.3) 17 | 18 | ### What's New 19 | 20 | * **This release contains breaking changes to the API**. This should hopefully be the last major breaking change to the API for a long time. 21 | * `TextTable` construction closure now takes an instance of `T` instead of a `Config`. `Config` has been removed completely. Instead of calling `column(...)` on a `Config` in the closure, you now return an array of `Column` instances. See example transition below for details. 22 | * **Fixed**: Missing/broken tests. 23 | * **Fixed**: Latex `tabular` environment contains incorrect alignments. 24 | * **Fixed**: Handle optional values in mapping. 25 | * **Fixed**: `FancyGrid` using pipe symbol instead of unicode drawing character for header separator. 26 | * All columns are now left-aligned by default (instead of right-aligning the first column by default). 27 | * `TextTableFormatter` has been renamed to `TextTableStyle` to avoid confusion with the Foundation [Formatters](https://developer.apple.com/reference/foundation/nsformatter) class. All references to the word "format" when referring to the type of table rendered have bee changed to "style". 28 | * The `TextTableStyle` protocol (formerly `TextTableFormatter `) is now simplified. All methods in `TextTableStyle` are now static, no state is maintained in `TextTableStyle`. The methods have also been consolidated (`row` instead of `beginRow`, `endRow`, `content`, `beginColumn`, etc...). 29 | * `TextTable` and all the internal types are now value types instead of classes. 30 | * All built-in styles are case-less `enums` so they cannot be accidentally instantiated. 31 | 32 | ### Known Issues 33 | 34 | * Very little effort has but put into performance optimizations. String utilities in particular. 35 | * It should be possible to create columns without headers, but this hasn't been tested and likely doesn't work yet. 36 | 37 | ### Transitioning 38 | 39 | Before: 40 | 41 | ```Swift 42 | let table = TextTable { t in 43 | t.column("Name") { $0.name } 44 | t.column("Age") { $0.age } 45 | t.column("Birthday") { $0.birhtday } 46 | } 47 | ``` 48 | 49 | After: 50 | 51 | ```Swift 52 | let table = TextTable { 53 | [Column("Name" <- $0.name), 54 | Column("Age" <- $0.age), 55 | Column("Birthday" <- $0.birhtday)] 56 | } 57 | ``` 58 | 59 | ### SPM Dependency Snippet 60 | 61 | ```Swift 62 | .Package(url: "https://github.com/cfilipov/TextTable", Version(1, 0, 0, prereleaseIdentifiers: ["alpha", "3"])) 63 | ``` 64 | 65 | ## [1.0.0-alpha.2](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.2) 66 | 67 | ### What's New 68 | 69 | * Column truncation support. There is now an optional `truncate:` argument to `width`. 70 | * Added some documentation. 71 | 72 | ### Known Issues 73 | 74 | * Very little effort has but put into performance optimizations. 75 | * It should be possible to create columns without headers, but this hasn't been tested and likely doesn't work yet. 76 | 77 | ### SPM Dependency Snippet 78 | 79 | ```Swift 80 | .Package(url: "https://github.com/cfilipov/TextTable", Version(1, 0, 0, prereleaseIdentifiers: ["alpha", "2"])) 81 | ``` 82 | 83 | ## [1.0.0-alpha.1](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.1) 84 | 85 | ### What's New 86 | 87 | * Support for center alignment. 88 | * If all columns have explicit width, then width calculations are skipped. 89 | * Escape strings in certain formats (HTML & Latex, for example). 90 | * Fixed: `TextTable` config persists calculated column widths. This results in widths getting stuck from the first call to `print` or `string(for:)`. 91 | 92 | ### Known Issues 93 | 94 | * Very little effort has but put into performance optimizations. 95 | * It should be possible to create columns without headers, but this hasn't been tested and likely doesn't work yet. 96 | 97 | ### SPM Dependency Snippet 98 | 99 | ```Swift 100 | .Package(url: "https://github.com/cfilipov/TextTable", Version(1, 0, 0, prereleaseIdentifiers: ["alpha", "1"])) 101 | ``` 102 | 103 | ## [1.0.0-alpha.0](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.0) 104 | 105 | ### What's New 106 | 107 | * Breaking change: completely re-written API. No more conforming to a protocol, instead a `TextTable` class is used in a similar way to `NSFormatter`. This offers much more flexibility. 108 | * Many more output formats. Similar to what you get from Python's [tabulate](https://pypi.python.org/pypi/tabulate) lib. 109 | 110 | ### Known Issues 111 | 112 | * No attempt at optimizing performance has been made yet. Even when widths are provided, an expensive calculation is still performed. 113 | * No escaping is being done. None of the formatters even attempt to sanitize the input strings. 114 | * Center alignment not supported. This will result in a `fatalError()`. 115 | * It should be possible to create columns without headers, but this hasn't been tested and likely doesn't work yet. 116 | 117 | ### SPM Dependency Snippet 118 | 119 | ```Swift 120 | .Package(url: "https://github.com/cfilipov/TextTable", Version(1, 0, 0, prereleaseIdentifiers: ["alpha", "0"])) 121 | ``` 122 | 123 | ## [0.0.0](https://github.com/cfilipov/TextTable/releases/tag/v0.0.0) 124 | 125 | Initial release. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cristian Filipov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TextTable", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "TextTable", 12 | targets: ["TextTable"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "TextTable", 23 | dependencies: []), 24 | .testTarget( 25 | name: "TextTableTests", 26 | dependencies: ["TextTable"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextTable 2 | 3 | [![Swift 5.0](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat)](https://swift.org) 4 | [![License MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://tldrlegal.com/license/mit-license) 5 | [![License MIT](https://img.shields.io/badge/SPM-1.0.0--alpha.4-red.svg?style=flat)](https://github.com/cfilipov/TextTable/releases/tag/v1.0.0-alpha.4) 6 | [![Build Status](https://github.com/cfilipov/TextTable/workflows/CI/badge.svg)](https://github.com/cfilipov/TextTable/actions) 7 | 8 | Easily print textual tables in Swift. Inspired by the Python [tabulate](https://pypi.python.org/pypi/tabulate) library. 9 | 10 | ## Upcoming Changes 11 | 12 | See [details on an upcoming change.](https://github.com/cfilipov/TextTable/issues/1) 13 | 14 | ## Features 15 | 16 | * Easily generate textual tables from collections of any type 17 | * Auto-calculates column width 18 | * Custom width for individual columns 19 | * Align individual columns 20 | * Supports per-column [Formatters](https://developer.apple.com/reference/foundation/nsformatter) 21 | * Multiple table output styles including Markdown, Emacs, HTML, Latex etc... 22 | * Extensible table styles allow you to easily create your own table style 23 | * Column-specific truncation modes (truncate head or tail of column content or optionally fail if content overflows). 24 | 25 | ## Requirements 26 | 27 | This package was written for and tested with the following version of Swift: 28 | 29 | > Apple Swift version 5.0 30 | 31 | ## Usage 32 | 33 | Use the [Swift Package Manager](https://swift.org/package-manager/) to install `TextTable` as a dependency of your project. 34 | 35 | ```Swift 36 | dependencies: [ 37 | .package( 38 | url: "https://github.com/cfilipov/TextTable", 39 | .branch("master")) 40 | ] 41 | ``` 42 | 43 | First, import `TextTable`: 44 | 45 | ```Swift 46 | import TextTable 47 | ``` 48 | 49 | Throughout the examples, the following data structure will be used: 50 | 51 | ```Swift 52 | struct Person { 53 | let name: String 54 | let age: Int 55 | let birhtday: Date 56 | } 57 | ``` 58 | 59 | Let's say we have a collection of `Person` we wish to display as a table. 60 | 61 | ```Swift 62 | let data = [ 63 | Person(name: "Alice", age: 42, birhtday: Date()), 64 | Person(name: "Bob", age: 22, birhtday: Date()), 65 | Person(name: "Eve", age: 142, birhtday: Date()) 66 | ] 67 | ``` 68 | 69 | ### Configure a TextTable Instance 70 | 71 | Configure a `TextTable` instance for the type you wish to print as a table. The minimum configuration will require a mapping of values to columns for your type. 72 | 73 | ```Swift 74 | let table = TextTable { 75 | [Column("Name" <- $0.name), 76 | Column("Age" <- $0.age), 77 | Column("Birthday" <- $0.birhtday)] 78 | } 79 | ``` 80 | 81 | #### Alternate Syntax 82 | 83 | The `<-` operator is just syntactic sugar. If you don't want to use the operator, you can use labeled arguments instead. 84 | 85 | ```Swift 86 | let table = TextTable { 87 | [Column(title: "Name", value: $0.name), 88 | Column(title: "Age", value: $0.age), 89 | Column(title: "Birthday", value: $0.birhtday)] 90 | } 91 | ``` 92 | 93 | ### Printing a Table 94 | 95 | Simply call the `print()` method, passing in a collection of `Person`. 96 | 97 | ```Swift 98 | table.print(data) 99 | ``` 100 | ``` 101 | Name Age Birthday 102 | ----- --- ------------------------- 103 | Alice 42 2016-08-13 19:21:54 +0000 104 | Bob 22 2016-08-13 19:21:54 +0000 105 | Eve 142 2016-08-13 19:21:54 +0000 106 | ``` 107 | 108 | ### Getting a String 109 | 110 | You can also just get the rendered table as a string by calling the `string(for:)` method. 111 | 112 | ```Swift 113 | let s = table.string(for: data) 114 | ``` 115 | 116 | ### Customize Table Style 117 | 118 | You can optionally specify a table style using the `style:` argument. 119 | 120 | ```Swift 121 | table.print(data, style: Style.psql) 122 | ``` 123 | ``` 124 | +-------+-----+---------------------------+ 125 | | Name | Age | Birthday | 126 | +-------+-----+---------------------------+ 127 | | Alice | 42 | 2016-08-13 08:12:58 +0000 | 128 | | Bob | 22 | 2016-08-13 08:12:58 +0000 | 129 | | Eve | 142 | 2016-08-13 08:12:58 +0000 | 130 | +-------+-----+---------------------------+ 131 | ``` 132 | 133 | The style can also be specified when using the `string(for:)` method. 134 | 135 | ```Swift 136 | let s = table.string(for: data, style: Style.psql) 137 | ``` 138 | 139 | For a full list of supported styles see [Supported Styles](#supported-styles) below. 140 | 141 | ### Column-Specific Formatters 142 | 143 | You can specify a [Formatter](https://developer.apple.com/reference/foundation/nsformatter) on a per-column basis. In this example a [DateFormatter](https://developer.apple.com/reference/foundation/nsdateformatter) is used to customize the display of the `Birthday` column. 144 | 145 | ```Swift 146 | let df = DateFormatter() 147 | dateFormatter.dateStyle = .medium 148 | 149 | let table = TextTable { 150 | [Column("Name" <- $0.name), 151 | Column("Age" <- $0.age), 152 | Column("Birthday" <- $0.birhtday, formatter: df)] 153 | } 154 | 155 | table.print(data, style: Style.psql) 156 | ``` 157 | ``` 158 | +-------+-----+--------------+ 159 | | Name | Age | Birthday | 160 | +-------+-----+--------------+ 161 | | Alice | 42 | Aug 13, 2016 | 162 | | Bob | 22 | Aug 13, 2016 | 163 | | Eve | 142 | Aug 13, 2016 | 164 | +-------+-----+--------------+ 165 | ``` 166 | 167 | ### Column Width 168 | 169 | By default TextTable will calculate the necessary width for each column. You can also explictely set the width with the `width:` argument. 170 | 171 | ```Swift 172 | let table = TextTable { 173 | [Column("Name" <- $0.name, width: 10), 174 | Column("Age" <- $0.age, width: 10)] 175 | } 176 | 177 | table.print(data, style: Style.psql) 178 | ``` 179 | ``` 180 | +------------+------------+ 181 | | Name | Age | 182 | +------------+------------+ 183 | | Alice | 42 | 184 | | Bob | 22 | 185 | | Eve | 142 | 186 | +------------+------------+ 187 | ``` 188 | 189 | #### Note on Padding 190 | 191 | Some table styles may include padding to the left and/or right of the content, the width argument does not effect include this padding. 192 | 193 | ### Truncation 194 | 195 | #### Truncate Tail 196 | 197 | By default, if a width is specified and the contents of the column are wider than the width, the text will be truncated at the tail (`.tail` truncation mode). 198 | 199 | ```Swift 200 | let table = TextTable { 201 | [Column("Name" <- $0.name, width: 4), // defaults to truncation: .tail 202 | Column("Age" <- $0.age)] 203 | } 204 | 205 | table.print(data, style: testStyle) 206 | ``` 207 | ``` 208 | +------+-----+ 209 | | Name | Age | 210 | +------+-----+ 211 | | Ali… | 42 | 212 | | Bob | 22 | 213 | | Eve | 142 | 214 | +------+-----+ 215 | ``` 216 | 217 | #### Truncate Head 218 | 219 | You can also truncate at the head of the text string by specifying `.head` for the `truncation:` argument. If no `width` argument is present, the `truncation` argument has no effect. 220 | 221 | ```Swift 222 | let table = TextTable { 223 | [Column("Name" <- $0.name, width: 4, truncate: .head), 224 | Column("Age" <- $0.age)] 225 | } 226 | 227 | table.print(data, style: testStyle) 228 | ``` 229 | ``` 230 | +------+-----+ 231 | | Name | Age | 232 | +------+-----+ 233 | | …ice | 42 | 234 | | Bob | 22 | 235 | | Eve | 142 | 236 | +------+-----+ 237 | ``` 238 | 239 | #### Truncate Error 240 | 241 | If you prefer to have an fatal error triggered when the contents of a column do not fit the specified width you can specify the `.error` truncation mode. 242 | 243 | ```Swift 244 | let table = TextTable { 245 | [Column("Name" <- $0.name, width: 4, truncate: .error), 246 | Column("Age" <- $0.age)] 247 | } 248 | 249 | table.print(data, style: testStyle) 250 | 251 | // fatal error: Truncation error 252 | ``` 253 | 254 | ### Column Alignment 255 | 256 | By default, all columns will be left-aligned. You can set the alignment on each column individually. 257 | 258 | ```Swift 259 | let table = TextTable { 260 | [Column("Name" <- $0.name), // default: .left 261 | Column("Age" <- $0.age, align: .right)] 262 | } 263 | 264 | table.print(data, style: Style.psql) 265 | ``` 266 | ``` 267 | +-------+-----+ 268 | | Name | Age | 269 | +-------+-----+ 270 | | Alice | 42 | 271 | | Bob | 22 | 272 | | Eve | 142 | 273 | +-------+-----+ 274 | ``` 275 | 276 | ## Performance Characteristics 277 | 278 | By default TextTable will calculate the necessary width for each column that does not specify an explicit width using the `width:` column argument. This calculation is accomplished by iterating over each row of the table twice: the first time to calculate the max width, the second time to actually render the table. The overhead of this calculation may not be appropriate for very large tables. If you wish to avoid this calculation you must specify the width for every column. 279 | 280 | In the example below `table1` will incur the overhead of the width calculation because one of the columns does not have an explicit width. On the other hand, `table2` will not incur the overhead because all columns have an explicit width. 281 | 282 | ```Swift 283 | // This will still result in extra calculations 284 | let table1 = TextTable { 285 | [Column("Name" <- $0.name), 286 | Column("Age" <- $0.age, width: 10)] 287 | } 288 | ``` 289 | ```Swift 290 | // This will skip the width calculations 291 | let table2 = TextTable { 292 | [Column("Name" <- $0.name, width: 9), 293 | Column("Age" <- $0.age, width: 10)] 294 | } 295 | ``` 296 | 297 | ## Supported Styles 298 | 299 | TextTable supports most of the styles as [tabulate](https://pypi.python.org/pypi/tabulate). 300 | 301 | ### Plain 302 | 303 | ``` 304 | table.print(data, style: Style.plain) 305 | ``` 306 | ``` 307 | Name Age Birthday 308 | Alice 42 8/13/16 309 | Bob 22 8/13/16 310 | Eve 142 8/13/16 311 | ``` 312 | 313 | ### Simple 314 | 315 | `simple` is the default Style. It corresponds to `simple_tables` in [Pandoc](http://pandoc.org/) Markdown extensions. 316 | 317 | ``` 318 | table.print(data) // or: 319 | table.print(data, style: Style.simple) 320 | ``` 321 | ``` 322 | Name Age Birthday 323 | ----- --- -------- 324 | Alice 42 8/13/16 325 | Bob 22 8/13/16 326 | Eve 142 8/13/16 327 | ``` 328 | 329 | ### Grid 330 | 331 | `grid` follows the conventions of Emacs’ [table.el package](https://www.emacswiki.org/emacs/TableMode). It corresponds to `grid_tables` in Pandoc Markdown extensions. 332 | 333 | ``` 334 | table.print(data, style: Style.grid) 335 | ``` 336 | ``` 337 | +-------+-----+----------+ 338 | | Name | Age | Birthday | 339 | +=======+=====+==========+ 340 | | Alice | 42 | 8/13/16 | 341 | +-------+-----+----------+ 342 | | Bob | 22 | 8/13/16 | 343 | +-------+-----+----------+ 344 | | Eve | 142 | 8/13/16 | 345 | +-------+-----+----------+ 346 | ``` 347 | 348 | ### FancyGrid 349 | 350 | ``` 351 | table.print(data, style: Style.fancyGrid) 352 | ``` 353 | ``` 354 | ╒═══════╤═════╤══════════╕ 355 | │ Name │ Age │ Birthday │ 356 | ╞═══════╪═════╪══════════╡ 357 | │ Alice │ 42 │ 8/13/16 │ 358 | ├───────┼─────┼──────────┤ 359 | │ Bob │ 22 │ 8/13/16 │ 360 | ├───────┼─────┼──────────┤ 361 | │ Eve │ 142 │ 8/13/16 │ 362 | ╘═══════╧═════╧══════════╛ 363 | ``` 364 | 365 | ### Psql 366 | 367 | ``` 368 | table.print(data, style: Style.psql) 369 | ``` 370 | ``` 371 | +-------+-----+----------+ 372 | | Name | Age | Birthday | 373 | +-------+-----+----------+ 374 | | Alice | 42 | 8/13/16 | 375 | | Bob | 22 | 8/13/16 | 376 | | Eve | 142 | 8/13/16 | 377 | +-------+-----+----------+ 378 | ``` 379 | 380 | ### Pipe 381 | 382 | `pipe` follows the conventions of [PHP Markdown Extra extension](https://michelf.ca/projects/php-markdown/extra/). It corresponds to `pipe_tables` in Pandoc. This style uses colons to indicate column alignment. 383 | 384 | ``` 385 | table.print(data, style: Style.pipe) 386 | ``` 387 | ``` 388 | | Name | Age | Birthday | 389 | |:------|----:|---------:| 390 | | Alice | 42 | 8/13/16 | 391 | | Bob | 22 | 8/13/16 | 392 | | Eve | 142 | 8/13/16 | 393 | ``` 394 | 395 | ### Org 396 | 397 | `org` follows the conventions of [Emacs org-mode](http://orgmode.org/). 398 | 399 | ``` 400 | table.print(data, style: Style.org) 401 | ``` 402 | ``` 403 | | Name | Age | Birthday | 404 | |-------+-----+----------| 405 | | Alice | 42 | 8/13/16 | 406 | | Bob | 22 | 8/13/16 | 407 | | Eve | 142 | 8/13/16 | 408 | ``` 409 | 410 | ### Rst 411 | 412 | `rst` follows the conventions of simple table of the [reStructuredText](http://docutils.sourceforge.net/rst.html) Style. 413 | 414 | ```Swift 415 | table.print(data, style: Style.rst) 416 | ``` 417 | ``` 418 | ===== === ======== 419 | Name Age Birthday 420 | ===== === ======== 421 | Alice 42 8/13/16 422 | Bob 22 8/13/16 423 | Eve 142 8/13/16 424 | ===== === ======== 425 | ``` 426 | 427 | ### Html 428 | 429 | `html` produces [HTML table](https://www.w3.org/TR/html5/tabular-data.html) markup. 430 | 431 | ``` 432 | table.print(data, style: Style.html) 433 | ``` 434 | ``` 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 |
NameAgeBirthday
Alice428/14/16
Bob228/14/16
Eve1428/14/16
457 | ``` 458 | 459 | ### Latex 460 | 461 | `latex` produces a [tabular](https://www.tug.org/TUGboat/tb28-3/tb90hoeppner.pdf) environment for [Latex](https://www.latex-project.org/about/). 462 | 463 | ``` 464 | table.print(data, style: Style.latex) 465 | ``` 466 | ``` 467 | \begin{tabular}{lll} 468 | \hline 469 | Name & Age & Birthday \\ 470 | \hline 471 | Alice & 42 & 8/13/16 \\ 472 | Bob & 22 & 8/13/16 \\ 473 | Eve & 142 & 8/13/16 \\ 474 | \hline 475 | \end{tabular} 476 | ``` 477 | 478 | ## Custom Styles 479 | 480 | You can create a custom table style by conforming to the `TextTableStyle` protocol. 481 | 482 | 483 | ```Swift 484 | enum MyCustomStyle: TextTableStyle { 485 | // implement methods from TextTableStyle... 486 | } 487 | 488 | extension Style { 489 | static let custom = MyCustomStyle.self 490 | } 491 | 492 | table.print(data, style: Style.custom) 493 | ``` 494 | 495 | ## Changes 496 | 497 | A list of changes can be found in the [CHANGELOG](CHANGELOG.md). 498 | 499 | ## Alternatives 500 | 501 | Other Swift libraries that serve a similar purpose. 502 | 503 | * [SwiftyTextTable](https://github.com/scottrhoyt/SwiftyTextTable) 504 | 505 | ## License 506 | 507 | MIT License 508 | 509 | Copyright (c) 2016 Cristian Filipov 510 | 511 | Permission is hereby granted, free of charge, to any person obtaining a copy 512 | of this software and associated documentation files (the "Software"), to deal 513 | in the Software without restriction, including without limitation the rights 514 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 515 | copies of the Software, and to permit persons to whom the Software is 516 | furnished to do so, subject to the following conditions: 517 | 518 | The above copyright notice and this permission notice shall be included in all 519 | copies or substantial portions of the Software. 520 | 521 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 522 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 523 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 524 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 525 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 526 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 527 | SOFTWARE. 528 | -------------------------------------------------------------------------------- /Sources/TextTable/FancyGridFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyGridFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum FancyGrid: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 24 | 25 | public static func end(_ table: inout String, index: Int, columns: [Column]) { 26 | table += "╘═" 27 | table += columns.map{$0.repeated("═")}.joined(separator: "═╧═") 28 | table += "═╛" 29 | table += "\n" 30 | } 31 | 32 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 33 | table += "╒═" 34 | table += columns.map{$0.repeated("═")}.joined(separator: "═╤═") 35 | table += "═╕" 36 | table += "\n" 37 | 38 | table += "│ " 39 | table += columns.map{$0.headerString(for: self)}.joined(separator: " │ ") 40 | table += " │" 41 | table += "\n" 42 | } 43 | 44 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 45 | if index == 0 { 46 | table += "╞═" 47 | table += columns.map{$0.repeated("═")}.joined(separator: "═╪═") 48 | table += "═╡" 49 | table += "\n" 50 | } else { 51 | table += "├─" 52 | table += columns.map{$0.repeated("─")}.joined(separator: "─┼─") 53 | table += "─┤" 54 | table += "\n" 55 | } 56 | 57 | table += "│ " 58 | table += columns.map{$0.string(for: self)}.joined(separator: " │ ") 59 | table += " │" 60 | table += "\n" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/TextTable/GridFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GrigFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Grid: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | private static func line(_ table: inout String, columns: [Column]) { 24 | table += "+-" 25 | table += columns.map{$0.repeated("-")}.joined(separator: "-+-") 26 | table += "-+" 27 | table += "\n" 28 | } 29 | 30 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 31 | public static func end(_ table: inout String, index: Int, columns: [Column]) { } 32 | 33 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 34 | line(&table, columns: columns) 35 | 36 | table += "| " 37 | table += columns.map{$0.headerString(for: self)}.joined(separator: " | ") 38 | table += " |" 39 | table += "\n" 40 | 41 | table += "+=" 42 | table += columns.map{$0.repeated("=")}.joined(separator: "=+=") 43 | table += "=+" 44 | table += "\n" 45 | } 46 | 47 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 48 | table += "| " 49 | table += columns.map{$0.string(for: self)}.joined(separator: " | ") 50 | table += " |" 51 | table += "\n" 52 | line(&table, columns: columns) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/TextTable/HtmlFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Html: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | } 17 | return escape(string) 18 | } 19 | 20 | public static func escape(_ s: String) -> String { 21 | return s 22 | .replacingOccurrences(of: "&", with: "&") 23 | .replacingOccurrences(of: "<", with: "<") 24 | .replacingOccurrences(of: "<", with: ">") 25 | } 26 | 27 | private static func string(for alignment: Alignment) -> String { 28 | switch alignment { 29 | case .left: return "left" 30 | case .right: return "right" 31 | case .center: return "center" 32 | } 33 | } 34 | 35 | private static func string(header col: Column) -> String { 36 | return " " + 37 | col.headerString(for: self) + "\n" 38 | } 39 | 40 | private static func string(row col: Column) -> String { 41 | return " " + col.string(for: self) + "\n" 42 | } 43 | 44 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { 45 | table += "\n" 46 | } 47 | 48 | public static func end(_ table: inout String, index: Int, columns: [Column]) { 49 | table += "
\n" 50 | } 51 | 52 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 53 | table += " \n" 54 | for col in columns { 55 | table += string(header: col) 56 | } 57 | table += " \n" 58 | } 59 | 60 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 61 | table += " \n" 62 | for col in columns { 63 | table += string(row: col) 64 | } 65 | table += " \n" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/TextTable/LatexFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LatexFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Latex: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { 22 | return s 23 | .replacingOccurrences(of: "#", with: "\\#") 24 | .replacingOccurrences(of: "$", with: "\\$") 25 | .replacingOccurrences(of: "%", with: "\\%") 26 | .replacingOccurrences(of: "&", with: "\\&") 27 | .replacingOccurrences(of: "\\", with: "\\textbackslash{}") 28 | .replacingOccurrences(of: "^", with: "\\textasciicircum{}") 29 | .replacingOccurrences(of: "_", with: "\\_") 30 | .replacingOccurrences(of: "{", with: "\\{") 31 | .replacingOccurrences(of: "}", with: "\\}") 32 | .replacingOccurrences(of: "~", with: "\\textasciitilde{}") 33 | // not strictly necessary, but makes for nicer output 34 | .replacingOccurrences(of: "…", with: "\\ldots ") 35 | } 36 | 37 | private static func alignments(_ col: Column) -> String { 38 | switch col.align { 39 | case .left: return "l" 40 | case .right: return "r" 41 | case .center: return "c" 42 | } 43 | } 44 | 45 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { 46 | let align = columns.map(alignments).joined(separator: "") 47 | table += "\\begin{tabular}{\(align)}\n" 48 | } 49 | 50 | public static func end(_ table: inout String, index: Int, columns: [Column]) { 51 | table += "\\hline\n" 52 | table += "\\end{tabular}\n" 53 | } 54 | 55 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 56 | table += "\\hline\n" 57 | table += " " 58 | table += columns.map{$0.headerString(for: self)}.joined(separator: " & ") 59 | table += " \\\\\n" 60 | table += "\\hline\n" 61 | } 62 | 63 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 64 | table += " " 65 | table += columns.map{$0.string(for: self)}.joined(separator: " & ") 66 | table += " \\\\\n" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/TextTable/OrgFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrgFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Org: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | private static func line(_ table: inout String, columns: [Column]) { 24 | table += "|-" 25 | table += columns.map{$0.repeated("-")}.joined(separator: "-+-") 26 | table += "-|" 27 | table += "\n" 28 | } 29 | 30 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 31 | public static func end(_ table: inout String, index: Int, columns: [Column]) { } 32 | 33 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 34 | table += "| " 35 | table += columns.map{$0.headerString(for: self)}.joined(separator: " | ") 36 | table += " |" 37 | table += "\n" 38 | line(&table, columns: columns) 39 | } 40 | 41 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 42 | table += "| " 43 | table += columns.map{$0.string(for: self)}.joined(separator: " | ") 44 | table += " |" 45 | table += "\n" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/TextTable/PipeFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PipeFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Pipe: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | private static func line(_ table: inout String, columns: [Column]) { 24 | table += "|" 25 | table += columns.map(column).joined(separator: "|") 26 | table += "|" 27 | table += "\n" 28 | } 29 | 30 | private static func column(_ col: Column) -> String { 31 | let w = max(col.width ?? 0, 3) 32 | switch col.align { 33 | case .left: 34 | return ":" + String(repeating: "-", count: w+1) 35 | case .right: 36 | return String(repeating: "-", count: w+1) + ":" 37 | case .center: 38 | return ":" + String(repeating: "-", count: w) + ":" 39 | } 40 | } 41 | 42 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 43 | public static func end(_ table: inout String, index: Int, columns: [Column]) { } 44 | 45 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 46 | table += "| " 47 | table += columns.map{$0.headerString(for: self)}.joined(separator: " | ") 48 | table += " |" 49 | table += "\n" 50 | line(&table, columns: columns) 51 | } 52 | 53 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 54 | table += "| " 55 | table += columns.map{$0.string(for: self)}.joined(separator: " | ") 56 | table += " |" 57 | table += "\n" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/TextTable/PlainFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Plain: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 23 | public static func end(_ table: inout String, index: Int, columns: [Column]) { } 24 | 25 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 26 | table += columns.map{$0.headerString(for: self)}.joined(separator: " ") 27 | table += "\n" 28 | } 29 | 30 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 31 | table += columns.map{$0.string(for: self)}.joined(separator: " ") 32 | table += "\n" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TextTable/PsqlFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PSQLFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Psql: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | private static func line(_ table: inout String, columns: [Column]) { 24 | table += "+-" 25 | table += columns.map{$0.repeated("-")}.joined(separator: "-+-") 26 | table += "-+" 27 | table += "\n" 28 | } 29 | 30 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { 31 | line(&table, columns: columns) 32 | } 33 | 34 | public static func end(_ table: inout String, index: Int, columns: [Column]) { 35 | line(&table, columns: columns) 36 | } 37 | 38 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 39 | table += "| " 40 | table += columns.map{$0.headerString(for: self)}.joined(separator: " | ") 41 | table += " |" 42 | table += "\n" 43 | line(&table, columns: columns) 44 | } 45 | 46 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 47 | table += "| " 48 | table += columns.map{$0.string(for: self)}.joined(separator: " | ") 49 | table += " |" 50 | table += "\n" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/TextTable/RstFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RstFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public enum Rst: TextTableStyle { 13 | public static func prepare(_ s: String, for column: Column) -> String { 14 | var string = s 15 | if let width = column.width { 16 | string = string.truncated(column.truncate, length: width) 17 | string = string.pad(column.align, length: width) 18 | } 19 | return escape(string) 20 | } 21 | 22 | public static func escape(_ s: String) -> String { return s } 23 | 24 | private static func line(_ table: inout String, columns: [Column]) { 25 | table += columns.map{$0.repeated("=")}.joined(separator: " ") 26 | table += "\n" 27 | } 28 | 29 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { 30 | line(&table, columns: columns) 31 | } 32 | 33 | public static func end(_ table: inout String, index: Int, columns: [Column]) { 34 | line(&table, columns: columns) 35 | } 36 | 37 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 38 | table += columns.map{$0.headerString(for: self)}.joined(separator: " ") 39 | table += "\n" 40 | line(&table, columns: columns) 41 | } 42 | 43 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 44 | table += columns.map{$0.string(for: self)}.joined(separator: " ") 45 | table += "\n" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/TextTable/SimpleFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleFormat.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Simple: TextTableStyle { 12 | public static func prepare(_ s: String, for column: Column) -> String { 13 | var string = s 14 | if let width = column.width { 15 | string = string.truncated(column.truncate, length: width) 16 | string = string.pad(column.align, length: width) 17 | } 18 | return escape(string) 19 | } 20 | 21 | public static func escape(_ s: String) -> String { return s } 22 | 23 | public static func begin(_ table: inout String, index: Int, columns: [Column]) { } 24 | 25 | public static func end(_ table: inout String, index: Int, columns: [Column]) { } 26 | 27 | public static func header(_ table: inout String, index: Int, columns: [Column]) { 28 | table += columns.map{$0.headerString(for: self)}.joined(separator: " ") 29 | table += "\n" 30 | 31 | table += columns.map{$0.repeated("-")}.joined(separator: " ") 32 | table += "\n" 33 | } 34 | 35 | public static func row(_ table: inout String, index: Int, columns: [Column]) { 36 | table += columns.map{$0.string(for: self)}.joined(separator: " ") 37 | table += "\n" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/TextTable/String+Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Padding.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | internal typealias PaddingFunction = (_ length: Int, _ character: Character) -> String 12 | 13 | internal extension String { 14 | mutating func append(_ c: String, repeat count: Int) { 15 | append(String(repeating: c, count: count)) 16 | } 17 | 18 | // https://github.com/coryalder/SwiftLeftpad 19 | func leftpad(length: Int, character: Character = " ") -> String { 20 | var outString: String = self 21 | let extraLength = length - outString.count 22 | var i = 0 23 | while (i < extraLength) { 24 | outString.insert(character, at: outString.startIndex) 25 | i += 1 26 | } 27 | return outString 28 | } 29 | 30 | func rightpad(length: Int, character: Character = " ") -> String { 31 | return padding(toLength: length, withPad: String(character), startingAt: 0) 32 | } 33 | 34 | func centerpad(length: Int, character: Character = " ") -> String { 35 | let leftlen = (length - count)/2 + count 36 | return leftpad(length: leftlen).rightpad(length: length) 37 | } 38 | 39 | func replaceAll(_ character: Character) -> String { 40 | var out = "" 41 | for _ in (0.. String { 48 | let padfunc: PaddingFunction 49 | switch align { 50 | case .left: padfunc = rightpad 51 | case .right: padfunc = leftpad 52 | case .center: padfunc = centerpad 53 | } 54 | return padfunc(length, " ") 55 | } 56 | 57 | func truncated(_ mode: Truncation, length: Int) -> String { 58 | switch mode { 59 | case .tail: 60 | guard count > length else { return self } 61 | return self[.. length else { return self } 64 | return "…" + self[index(endIndex, offsetBy: -1*(length-1))...] 65 | case .error: 66 | guard count <= length else { return self } 67 | fatalError("Truncation error") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/TextTable/TextTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextTablePrintable.swift 3 | // Stooli 4 | // 5 | // Created by Cristian Filipov on 8/8/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | infix operator <- : AssignmentPrecedence 12 | 13 | public func <- (left: String, right: Any) -> (title: String, value: Any) { 14 | return (left, right) 15 | } 16 | 17 | private func + (left: [Column], right: [Int]) -> [Column] { 18 | precondition(left.count == right.count) 19 | return zip(left, right).map { $0.0.settingWidth($0.1) } 20 | } 21 | 22 | private func unwrap(_ any: Any) -> Any { 23 | let m = Mirror(reflecting: any) 24 | if m.displayStyle != .optional { return any } 25 | if m.children.count == 0 { return "NULL" } 26 | let (_, some) = m.children.first! 27 | return some 28 | } 29 | 30 | public enum Alignment { 31 | case left 32 | case right 33 | case center 34 | } 35 | 36 | public enum Truncation { 37 | case tail 38 | case head 39 | case error 40 | } 41 | 42 | /** 43 | The style pseudo-namespace groups together all the built-in styles that come siwht TextTable. You can add you own styles by extending the `Style` type. 44 | */ 45 | public enum Style { 46 | public static let simple = Simple.self 47 | public static let plain = Plain.self 48 | public static let rst = Rst.self 49 | public static let psql = Psql.self 50 | public static let pipe = Pipe.self 51 | public static let org = Org.self 52 | public static let latex = Latex.self 53 | public static let html = Html.self 54 | public static let grid = Grid.self 55 | public static let fancy = FancyGrid.self 56 | } 57 | 58 | /** 59 | A `Column` defines how a particular piece of data will be rendered in the text table. 60 | */ 61 | public struct Column { 62 | let title: String 63 | let value: Any 64 | let width: Int? 65 | let align: Alignment 66 | let truncate: Truncation 67 | let formatter: Formatter? 68 | 69 | public init(title: String, value: Any, width: Int? = nil, align: Alignment = .left, truncate: Truncation = .tail, formatter: Formatter? = nil) { 70 | self.title = title 71 | self.value = unwrap(value) 72 | self.width = width 73 | self.align = align 74 | self.truncate = truncate 75 | self.formatter = formatter 76 | } 77 | 78 | public init(_ mapping: @autoclosure () -> (String, Any), width: Int? = nil, align: Alignment = .left, truncate: Truncation = .tail, formatter: Formatter? = nil) { 79 | let (t, v) = mapping() 80 | self = Column( 81 | title: t, 82 | value: v, 83 | width: width, 84 | align: align, 85 | truncate: truncate, 86 | formatter: formatter) 87 | } 88 | 89 | internal var resolvedWidth: Int { 90 | return width ?? title.count 91 | } 92 | 93 | // private func string(for value: Any) -> String { 94 | // var string = String(describing: value) 95 | // if let formatter = formatter { 96 | // string = formatter.string(for: value) ?? string 97 | // } 98 | // return string 99 | // } 100 | 101 | fileprivate func settingWidth(_ newWidth: Int) -> Column { 102 | return Column( 103 | title: self.title, 104 | value: self.value, 105 | width: newWidth, 106 | align: self.align, 107 | truncate: self.truncate, 108 | formatter: self.formatter) 109 | } 110 | } 111 | 112 | public extension Column { 113 | func string(for style: TextTableStyle.Type) -> String { 114 | var string = "" 115 | if let formatter = formatter { 116 | string = formatter.string(for: value) ?? string 117 | } else { 118 | string = String(describing: value) 119 | } 120 | return style.prepare(string, for: self) 121 | } 122 | 123 | func headerString(for style: TextTableStyle.Type) -> String { 124 | return style.prepare(title, for: self) 125 | } 126 | 127 | func repeated(_ string: String) -> String { 128 | return String(repeating: string, count: resolvedWidth) 129 | } 130 | } 131 | 132 | /** 133 | The `TextTableStyle` protocol defines a set of static methods used to render a textual table. Styles are meant to be stateless and re-usable and are not meant to be instantiated. For this reason it is recomended to use case-less enums for your custom styles. 134 | 135 | Each static method accepts an inout `String` argument which is to be appended to as the output and an array of columns. 136 | */ 137 | public protocol TextTableStyle { 138 | /** 139 | Prepare the string to be rendered. The string may be the header text or column value. Typically this method will pad, truncate and call `escape()` on the string. If no special preperation is required for this style, you can simply return the passed-in string. 140 | */ 141 | static func prepare(_ s: String, for column: Column) -> String 142 | 143 | /** 144 | Escape any special characters from the provided string. Most styles simply return the passed-in string. However, in some cases you might want to escape certain characters that have special meaning in your output format. For example, the HTML style will escape `<`, `>` and other reserved HTML characters. 145 | */ 146 | static func escape(_ table: String) -> String 147 | 148 | /** 149 | Called before the header or any of the rows. 150 | */ 151 | static func begin(_ table: inout String, index: Int, columns: [Column]) 152 | 153 | /** 154 | Called after all the rows have completed. 155 | */ 156 | static func end(_ table: inout String, index: Int, columns: [Column]) 157 | 158 | /** 159 | Called just before the first row. 160 | */ 161 | static func header(_ table: inout String, index: Int, columns: [Column]) 162 | 163 | /** 164 | Called for each row. 165 | */ 166 | static func row(_ table: inout String, index: Int, columns: [Column]) 167 | } 168 | 169 | /** 170 | `TextTable` formats a string into a textual representation of a table. `TextTable` can render tables in various different styles including HTML, Markdown and Latex tables. 171 | 172 | Instances of `TextTable` represent the mapping of a type to a tabular representation. That is, `TextTable` defines how an instance of `T` can be transformed into a set of columns. 173 | 174 | A `TextTable` is configured by returning an array of `Column` instances in the closure argument of the initializer. Each `Column` may specify an width, alignment, truncation mode and NSFormatter. By default, widths are calculated based on the longest content and alignment defaults to left. 175 | 176 | ## Basic Usage 177 | 178 | struct Person { 179 | let name: String 180 | let age: Int 181 | let birhtday: Date 182 | } 183 | 184 | let df = DateFormatter() 185 | dateFormatter.dateStyle = .short 186 | 187 | let table = TextTable { 188 | [Column("Name" <- $0.name), 189 | Column("Age" <- $0.age, width: 6, align: .center), 190 | Column("Birthday" <- $0.birhtday, formatter: df)] 191 | } 192 | 193 | table.print(data) 194 | 195 | Output: 196 | 197 | Name Age Birthday 198 | ----- ------ -------- 199 | Alice 42 8/13/16 200 | Bob 22 8/13/16 201 | Eve 142 8/13/16 202 | */ 203 | public struct TextTable { 204 | /** 205 | The adapter function is responsible for mapping an instance `T` to an array of columns. This will be called for each row, including the header. The adapter function may actually be called multiple times for each row or header. You should not make any assumptions regarding the context, order or number of calls. 206 | */ 207 | public typealias Adapter = (T) -> [Column] 208 | 209 | private let adapter: Adapter 210 | 211 | /** 212 | Creates an instance of `TextTable` which is used to format strings of tables. This instance represents the mapping of a type `T` to its corresponding columns. An instance can be re-used (and should be re-used unless you need to change the mapping or column configuration). 213 | */ 214 | public init(_ adapter: @escaping Adapter) { 215 | self.adapter = adapter 216 | } 217 | 218 | private func calculateWidths(for data: C, style: TextTableStyle.Type) -> [Int] where C.Iterator.Element == T { 219 | guard let first = data.first else { return [] } 220 | let headerCols = adapter(first) 221 | var widths = headerCols.map{$0.width ?? 0} 222 | for (index,column) in headerCols.enumerated() { 223 | if let w = column.width { 224 | widths[index] = w 225 | } else { 226 | let text = column.headerString(for: style) 227 | widths[index] = max(text.count, widths[index]) 228 | } 229 | } 230 | for element in data { 231 | let cols = adapter(element) 232 | for (index, column) in cols.enumerated() { 233 | if let w = column.width { 234 | widths[index] = w 235 | } else { 236 | let text = column.string(for: style) 237 | widths[index] = max(text.count, widths[index]) 238 | } 239 | } 240 | } 241 | return widths 242 | } 243 | 244 | /** 245 | Returns a string representing the data rendered as a textual table. 246 | 247 | - parameter data: A collection of elements `T` for which this `TextTable` instance has been configured. 248 | - parameter style: The style of table to be rendered. See `Style` for more options. 249 | */ 250 | public func string(for data: C, style: TextTableStyle.Type = Style.simple) -> String? where C.Iterator.Element == T { 251 | guard let first = data.first else { return nil } 252 | var table = "" 253 | let cols = adapter(first) 254 | var widths = cols.compactMap{$0.width} 255 | if widths.count < cols.count { 256 | widths = calculateWidths(for: data, style: style) 257 | } 258 | style.begin(&table, index: -1, columns: cols + widths) 259 | style.header(&table, index: -1, columns: cols + widths) 260 | for (index, element) in data.enumerated() { 261 | style.row(&table, index: index, columns: adapter(element) + widths) 262 | } 263 | style.end(&table, index: -1, columns: cols + widths) 264 | return table 265 | } 266 | 267 | /** 268 | Prints the data rendered as a textual table. 269 | 270 | - parameter data: A collection of elements `T` for which this `TextTable` instance has been configured. 271 | - parameter style: The style of table to be rendered. See `Style` for more options. 272 | */ 273 | public func print(_ data: C, style: TextTableStyle.Type = Style.simple) where C.Iterator.Element == T { 274 | let table = string(for: data, style: style)! 275 | Swift.print(table) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * [ ] Support head/tail truncation without ellipsis. 4 | * [ ] Columns without title. This can be done now by doing `Column("" <- $.foo)`, but this is hacky. It should be possible to do this: `Column($foo)`. 5 | * [ ] Header-less tables. Not all styles may support this. 6 | * [ ] Table footers. Not all styles can support this, but in those cases the data can simply be ignored. 7 | * [ ] Profile performance. Don't know if it's fast or slow right now. Memory usage should be OK and it shouldn't be too bad, but string utils were written naively. 8 | * [ ] `maxWidth:` column argument. Unlike `width:`, this won't prevent width calculations, but instead it will limit how wide a column can get. If the contents of the column are less than the max width argument the column will be as wide as the calculated width. 9 | * [ ] Better escaping. Right now only Latex and HTML styles escape the contents of the table. Markdown and others probably need this too. 10 | * [ ] Customize `NULL` output. Currently, if an optional column value is `nil`, the column will read `NULL`. This should be configurable. -------------------------------------------------------------------------------- /Tests/TextTableTests/FancyGridFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyGridFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.fancy 13 | 14 | class FancyGridFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "╒═══════╤════════╤══════════╤════════════╤════════════╕\n" + 25 | "│ Name │ Age │ Birthday │ Notes │ Notes │\n" + 26 | "╞═══════╪════════╪══════════╪════════════╪════════════╡\n" + 27 | "│ Alice │ 42 │ 8/14/16 │ Lorem ips… │ … rhoncus. │\n" + 28 | "├───────┼────────┼──────────┼────────────┼────────────┤\n" + 29 | "│ Bob │ 22 │ 8/14/16 │ Nunc vari… │ …enenatis. │\n" + 30 | "├───────┼────────┼──────────┼────────────┼────────────┤\n" + 31 | "│ Eve │ 142 │ 8/14/16 │ Etiam qui… │ …ulus mus. │\n" + 32 | "╘═══════╧════════╧══════════╧════════════╧════════════╛\n" + 33 | "" 34 | let s = table.string(for: data, style: testStyle)! 35 | XCTAssertEqual(s, expectedOutput) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Tests/TextTableTests/GridFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.grid 13 | 14 | class GridFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "+-------+--------+----------+------------+------------+\n" + 25 | "| Name | Age | Birthday | Notes | Notes |\n" + 26 | "+=======+========+==========+============+============+\n" + 27 | "| Alice | 42 | 8/14/16 | Lorem ips… | … rhoncus. |\n" + 28 | "+-------+--------+----------+------------+------------+\n" + 29 | "| Bob | 22 | 8/14/16 | Nunc vari… | …enenatis. |\n" + 30 | "+-------+--------+----------+------------+------------+\n" + 31 | "| Eve | 142 | 8/14/16 | Etiam qui… | …ulus mus. |\n" + 32 | "+-------+--------+----------+------------+------------+\n" + 33 | "" 34 | let s = table.string(for: data, style: testStyle)! 35 | XCTAssertEqual(s, expectedOutput) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Tests/TextTableTests/HtmlFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLFormattests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.html 13 | 14 | class HtmlFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "\n" + 25 | " \n" + 26 | " \n" + 27 | " \n" + 28 | " \n" + 29 | " \n" + 30 | " \n" + 31 | " \n" + 32 | " \n" + 33 | " \n" + 34 | " \n" + 35 | " \n" + 36 | " \n" + 37 | " \n" + 38 | " \n" + 39 | " \n" + 40 | " \n" + 41 | " \n" + 42 | " \n" + 43 | " \n" + 44 | " \n" + 45 | " \n" + 46 | " \n" + 47 | " \n" + 48 | " \n" + 49 | " \n" + 50 | " \n" + 51 | " \n" + 52 | " \n" + 53 | "
NameAgeBirthdayNotesNotes
Alice428/14/16Lorem ips…… rhoncus.
Bob228/14/16Nunc vari……enenatis.
Eve1428/14/16Etiam qui……ulus mus.
\n" + 54 | "" 55 | let s = table.string(for: data, style: testStyle)! 56 | XCTAssertEqual(s, expectedOutput) 57 | table.print(data, style: testStyle) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Tests/TextTableTests/LatexFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LatexFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.latex 13 | 14 | class LatexFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "\\begin{tabular}{lcrrr}\n" + 25 | "\\hline\n" + 26 | " Name & Age & Birthday & Notes & Notes \\\\\n" + 27 | "\\hline\n" + 28 | " Alice & 42 & 8/14/16 & Lorem ips\\ldots & \\ldots rhoncus. \\\\\n" + 29 | " Bob & 22 & 8/14/16 & Nunc vari\\ldots & \\ldots enenatis. \\\\\n" + 30 | " Eve & 142 & 8/14/16 & Etiam qui\\ldots & \\ldots ulus mus. \\\\\n" + 31 | "\\hline\n" + 32 | "\\end{tabular}\n" + 33 | "" 34 | let s = table.string(for: data, style: testStyle)! 35 | XCTAssertEqual(s, expectedOutput) 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Tests/TextTableTests/OrgFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrgFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.org 13 | 14 | class OrgFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "| Name | Age | Birthday | Notes | Notes |\n" + 25 | "|-------+--------+----------+------------+------------|\n" + 26 | "| Alice | 42 | 8/14/16 | Lorem ips… | … rhoncus. |\n" + 27 | "| Bob | 22 | 8/14/16 | Nunc vari… | …enenatis. |\n" + 28 | "| Eve | 142 | 8/14/16 | Etiam qui… | …ulus mus. |\n" + 29 | "" 30 | let s = table.string(for: data, style: testStyle)! 31 | XCTAssertEqual(s, expectedOutput) 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Tests/TextTableTests/PipeFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PipeFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.pipe 13 | 14 | class PipeFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "| Name | Age | Birthday | Notes | Notes |\n" + 25 | "|:------|:------:|---------:|-----------:|-----------:|\n" + 26 | "| Alice | 42 | 8/14/16 | Lorem ips… | … rhoncus. |\n" + 27 | "| Bob | 22 | 8/14/16 | Nunc vari… | …enenatis. |\n" + 28 | "| Eve | 142 | 8/14/16 | Etiam qui… | …ulus mus. |\n" + 29 | "" 30 | let s = table.string(for: data, style: testStyle)! 31 | XCTAssertEqual(s, expectedOutput) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tests/TextTableTests/PlainFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.plain 13 | 14 | class PlainFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "Name Age Birthday Notes Notes\n" + 25 | "Alice 42 8/14/16 Lorem ips… … rhoncus.\n" + 26 | "Bob 22 8/14/16 Nunc vari… …enenatis.\n" + 27 | "Eve 142 8/14/16 Etiam qui… …ulus mus.\n" + 28 | "" 29 | let s = table.string(for: data, style: testStyle)! 30 | XCTAssertEqual(s, expectedOutput) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/TextTableTests/PsqlFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PSQLFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.psql 13 | 14 | class PsqlFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "+-------+--------+----------+------------+------------+\n" + 25 | "| Name | Age | Birthday | Notes | Notes |\n" + 26 | "+-------+--------+----------+------------+------------+\n" + 27 | "| Alice | 42 | 8/14/16 | Lorem ips… | … rhoncus. |\n" + 28 | "| Bob | 22 | 8/14/16 | Nunc vari… | …enenatis. |\n" + 29 | "| Eve | 142 | 8/14/16 | Etiam qui… | …ulus mus. |\n" + 30 | "+-------+--------+----------+------------+------------+\n" + 31 | "" 32 | let s = table.string(for: data, style: testStyle)! 33 | XCTAssertEqual(s, expectedOutput) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/TextTableTests/RstFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RstFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.rst 13 | 14 | class RstFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "===== ====== ======== ========== ==========\n" + 25 | "Name Age Birthday Notes Notes\n" + 26 | "===== ====== ======== ========== ==========\n" + 27 | "Alice 42 8/14/16 Lorem ips… … rhoncus.\n" + 28 | "Bob 22 8/14/16 Nunc vari… …enenatis.\n" + 29 | "Eve 142 8/14/16 Etiam qui… …ulus mus.\n" + 30 | "===== ====== ======== ========== ==========\n" + 31 | "" 32 | let s = table.string(for: data, style: testStyle)! 33 | XCTAssertEqual(s, expectedOutput) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/TextTableTests/SimpleFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleFormatTests.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import TextTable 11 | 12 | private let testStyle = Style.simple 13 | 14 | class SimpleFormatTests: XCTestCase { 15 | 16 | func testPerformanceExample() { 17 | self.measure { 18 | let _ = table.string(for: data, style: testStyle) 19 | } 20 | } 21 | 22 | func testTable() { 23 | let expectedOutput = "" + 24 | "Name Age Birthday Notes Notes\n" + 25 | "----- ------ -------- ---------- ----------\n" + 26 | "Alice 42 8/14/16 Lorem ips… … rhoncus.\n" + 27 | "Bob 22 8/14/16 Nunc vari… …enenatis.\n" + 28 | "Eve 142 8/14/16 Etiam qui… …ulus mus.\n" + 29 | "" 30 | let s = table.string(for: data, style: testStyle)! 31 | XCTAssertEqual(s, expectedOutput) 32 | let s2 = table.string(for: data)! 33 | XCTAssertEqual(s2, expectedOutput) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/TextTableTests/TestUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUtil.swift 3 | // TextTable 4 | // 5 | // Created by Cristian Filipov on 8/13/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import TextTable 11 | 12 | struct Person { 13 | let name: String 14 | let age: Int 15 | let birhtday: Date 16 | let notes: String 17 | } 18 | 19 | let df: DateFormatter = { 20 | let dateFormatter = DateFormatter() 21 | dateFormatter.dateStyle = .short 22 | return dateFormatter 23 | }() 24 | 25 | let data = [ 26 | Person( 27 | name: "Alice", 28 | age: 42, 29 | birhtday: df.date(from: "8/14/2016")!, 30 | notes: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ultrices a orci quis rhoncus."), 31 | Person( 32 | name: "Bob", 33 | age: 22, 34 | birhtday: df.date(from: "8/14/2016")!, 35 | notes: "Nunc varius lectus eget feugiat euismod. Mauris tincidunt turpis a augue varius, bibendum cursus elit venenatis."), 36 | Person( 37 | name: "Eve", 38 | age: 142, 39 | birhtday: df.date(from: "8/14/2016")!, 40 | notes: "Etiam quis lectus vestibulum, cursus lectus at, consectetur enim. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.") 41 | ] 42 | 43 | let table = TextTable { person in 44 | [Column("Name" <- person.name), 45 | Column("Age" <- person.age, width: 6, align: .center), 46 | Column("Birthday" <- person.birhtday, align: .right, formatter: df), 47 | Column("Notes" <- person.notes, width: 10, align: .right), 48 | Column("Notes" <- person.notes, width: 10, align: .right, truncate: .head)] 49 | } 50 | --------------------------------------------------------------------------------