Skip to content

Commit

Permalink
Merge pull request #1 from 3squared/opensource
Browse files Browse the repository at this point in the history
Opensource
  • Loading branch information
lukestringer90 authored Feb 16, 2021
2 parents 9a76158 + c864ba1 commit be8fc1b
Show file tree
Hide file tree
Showing 19 changed files with 424 additions and 133 deletions.
Binary file added Assets/diagram.key
Binary file not shown.
Binary file added Assets/diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Assets/example.png
Binary file not shown.
Binary file modified Assets/example.xlsx
Binary file not shown.
21 changes: 21 additions & 0 deletions Assets/people.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
first_name,gender,title,age,age_range
Hebert,Male,Dr,42,40s
Lon,Male,,25,20s
Ode,,Dr,63,60s
Ferguson,Male,,63,60s
Else,Female,Honorable,29,20s
Rennie,Female,,41,40s
Thorin,,,58,50s
Kaleena,Female,,57,50s
Konstance,,Mr,21,20s
Lorna,Female,Dr,38,30s
Bonni,Female,,68,60s
Morie,Male,Rev,44,40s
Briana,,Rev,40,40s
Vania,Female,Honorable,54,50s
Kent,Male,,52,50s
Page,,,35,30s
Kelley,Female,Ms,33,30s
Teriann,Female,Rev,36,30s
Nedda,Female,Mrs,45,40s
Frankie,Male,Mr,54,50s
Binary file added Assets/people.xlsx
Binary file not shown.
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright (c) 3Squared Ltd

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2 changes: 1 addition & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pins": [
{
"package": "SwiftCSV",
"repositoryURL": "git@github.com:swiftcsv/SwiftCSV.git",
"repositoryURL": "https://github.com/swiftcsv/SwiftCSV.git",
"state": {
"branch": null,
"revision": "ad2042d0ba2ea260246b0e1b1d5710cbc53e2836",
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ let package = Package(
],
dependencies: [
.package(
url: "git@github.com:swiftcsv/SwiftCSV.git",
.exact("0.6.0")
url: "https://github.com/swiftcsv/SwiftCSV.git",
from: ("0.6.0")
),
],
targets: [
Expand Down
175 changes: 170 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,176 @@
# PeakPivot

PeakPivot proives types and business logic for creating a pivot table from a CSV.
![Swift](https://github.com/3squared/PeakPivot/workflows/Swift/badge.svg?branch=main)

## Instalation
[Wikipedia](https://en.wikipedia.org/wiki/Pivot_table) says:

## Usage
> Pivot tables are a technique in data processing. They arrange and rearrange (or "pivot") statistics in order to draw attention to useful information. This leads to finding figures and facts quickly making them integral to data analysis.
## Licence
PeakPivot is a pure-swift implementation of pivot tables modelled on the implementation offered by Microsoft Excell. PeakPivot provides the base types and business logic for summarising tabular data into a pivot table.

## Example
## Types

PeakPivot defines a number of types that model the data necessary for building a pivot table, configuring a pivot table builder, and the resulting output pivot table.

Below is screenshot of a pivot table built in Microsoft Excel from the [`people.csv`](Assets/people.csv) file. It is annotated to show the corresponding PeakPivot types.

![](Assets/diagram.png)

### Input

These types are the input to the pivot building logic. They are highlighted in blue in the [diagram above](#Types).

`Table`. A array of `TableRow`s. This is the input to data to be pivotted. For example this may come from a CSV file.

`TableRow`. A key-value pair of `[FieldName : FieldValue]`. This represents a single row in a table. If using a CSV the `FieldName` corresponds to the column-name, and the `FieldValue` corresponds to the column-value for that row.

`FieldName`. A string representing a column-name in the input table.

`FieldValue`. A string representing a cell-value for a given column and row. Note that `String` is used as the type here to make it easier for CSV parsing and conforming `FieldValue` to `Equatable` (necessary for unit testing). Any non-string value can be boxed into a `FieldValue` if required.

### Builder Configuration

These types configure the pivot that is built. They are highlighted in green in the [diagram above](#Types).

`BuildPivot`. A protocol defining the business logic for building a pivot from a supplied `Table` and array of `FieldName`s to pivot around. A protocol extension provides a default implementation of the `build()` function. Either conform a type to `BuildPivot` or use the provided concrete implementation `PivotBuilder`.

`PivotBuilder`. A concrete implementation of the `BuildPivot` protocol that provides defaults for the configuration varaibles.

`FieldName`. A string representing a column-name from the input table to pivot around.

`BuildPivotFilter`. Describes how to apply a filter to a pivot table to only include certain `FieldName`s and values

`BuildPivotDescriptor`. Describes how to apply a sort to pivot table.

### Output

These types are the output from the pivot building logic. They are highlighted in red in the [diagram above](#Types).

`Pivot`. A pivot table, generated by processing a `Table` using a `BuildPivot`. Defines the `PivotRow`s in the output table, the `FilterField`s that can be used for subsequent filting, and the total number of rows in the table.

`PivotRow`. A row in a pivot table, that stores the computed value information (the count, sum, percentage etc), what level in the overall table is sits at, it's title (corresponds to the `FieldName` in the input `Table`), and any subrows it has. The subrows represent the nesting of groups in a pivot table. The number of levels to this nesting is equal to (number of `FieldName`s - 1) as passed into the `BuildPivot`.

`FilterField`. All the fields from a generated pivot table that can be used to filter the pivot table on a subsequent build. Use these `FilterField`s to create `BuildPivotFilter`s as needed.

## Building a Pivot

To recreate the pivot table shown [Excell spreadsheet above](#Types) we load in the [`people.csv`](Assets/people.csv) csv file using SwiftCSV (including with PeakPivot) and construct a `PivotBuilder` with the corresponding input and configuration data.

Set the `.fields` variable to the `FieldName`s you want to group the pivot table using. The last `FieldName` in the array defines the `FieldValue`s in the input `Table` to summarise using sum, count and percentage operators.

Once configured call the `build()` function.

```swift
do {
let csvURL = URL(fileURLWithPath: "url/to/people.csv")
let csv = try CSV(url: csvURL)
let csvRows = csv.namedRows

let builder = PivotBuilder()
builder.table = csvRows
builder.fields = ["title", "age"] // "age" is summarised
builder.sortDescriptor = .byTitle(ascending: false)
builder.filters = [BuildPivotFilter(fieldName: "title", include: ["Ms", "Mrs", "Mr", "Honorable", "Dr"])] // exclude "Blank" and "Rev" from the pivot table
builder.sumsEnabled = true // compute sums
builder.percentagesEnabled = true // compute percentages

let pivot = try builder.build() // run and catch any errors

// pivot.rows will equal
let pivotRows = [
PivotRow(level: 0, title: "Ms", value: PivotRow.Value(count: 1, sum: 33, percentage: 1/9), subRows: [
PivotRow(level: 1, title: "33", value: PivotRow.Value(count: 1, sum: 33, percentage: 1/9), subRows: nil),
]),
PivotRow(level: 0, title: "Mrs", value: PivotRow.Value(count: 1, sum: 45, percentage: 1/9), subRows: [
PivotRow(level: 1, title: "45", value: PivotRow.Value(count: 1, sum: 45, percentage: 1/9), subRows: nil),
]),
PivotRow(level: 0, title: "Mr", value: PivotRow.Value(count: 2, sum: 75, percentage: 2/9), subRows: [
PivotRow(level: 1, title: "54", value: PivotRow.Value(count: 1, sum: 54, percentage: 1/9), subRows: nil),
PivotRow(level: 1, title: "21", value: PivotRow.Value(count: 1, sum: 21, percentage: 1/9), subRows: nil),
]),
PivotRow(level: 0, title: "Honorable", value: PivotRow.Value(count: 2, sum: 83, percentage: 2/9), subRows: [
PivotRow(level: 1, title: "54", value: PivotRow.Value(count: 1, sum: 54, percentage: 1/9), subRows: nil),
PivotRow(level: 1, title: "29", value: PivotRow.Value(count: 1, sum: 29, percentage: 1/9), subRows: nil),
]),
PivotRow(level: 0, title: "Dr", value: PivotRow.Value(count: 3, sum: 143, percentage: 3/9), subRows: [
PivotRow(level: 1, title: "63", value: PivotRow.Value(count: 1, sum: 63, percentage: 1/9), subRows: nil),
PivotRow(level: 1, title: "42", value: PivotRow.Value(count: 1, sum: 42, percentage: 1/9), subRows: nil),
PivotRow(level: 1, title: "38", value: PivotRow.Value(count: 1, sum: 38, percentage: 1/9), subRows: nil),
])

// pivot.total will equal
let pivotTotal = 9

// pivot.fieldFields will equal
let filterFields = [
FilterField(name: "title", values: [
"Dr",
"Honorable",
"Mr",
"Mrs",
"Ms",
"Rev",
Blank
]),
FilterField(name: "age", values: [
"21",
"25",
"29",
"33",
"35",
"36",
"38",
"40",
"41",
"42",
"44",
"45",
"52",
"54",
"57",
"58",
"63",
"68"
])
]

]

} else {
// Handle error
}

```

For more examples of how to use PeakPivot see the [unit tests](Tests/PeakPivotTests). There is also a basic [example iOS project](Example) that uses PeakPivot.

## Installation

PeakPivot supports the Swift Package Manager. Add the PeakPivot dependency in your `package.swift`.

```swift
dependencies: [
.package(
url: "https://github.com/3squared/PeakPivot.git",
.exact("0.1.0")
),
]
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details

## Acknowledgments

* [SwiftCSV](https://github.com/swiftcsv/SwiftCSV)

# Peak Framework

The Peak Framework is a collection of open-source microframeworks created by the team at [3Squared](https://github.com/3squared), named for the [Peak District](https://en.wikipedia.org/wiki/Peak_District). It is made up of:

|Name|Description|
|:--|:--|
|[PeakOperation](https://github.com/3squared/PeakOperation)|Provides enhancement and conveniences to `Operation`, making use of the `Result` type.|
|[PeakNetwork](https://github.com/3squared/PeakNetwork)|A networking framework built on top of `Session` using PeakOperation, leveraging the power of `Codable`.|
|[PeakCoreData](https://github.com/3squared/PeakCoreData)|Provides enhances and conveniences to `Core Data`.|
39 changes: 38 additions & 1 deletion Sources/PeakPivot/BuildPivot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,47 @@ public protocol BuildPivot {
}

/// Errors generated during the pivot building process
public enum BuildPivotError: Error {
public enum BuildPivotError: Error, LocalizedError {

/// No table set
case noTable

/// No fields set
case noFields

/// Output fields failed to be generated
case noOutput

/// A localized message describing what error occurred.
public var errorDescription: String? {
get {
switch self {
case .noTable: return "No `table` variable was set so a pivot could not be built."
case .noFields: return "No `fields` variable was set so a pivot could not be built."
case .noOutput: return "The input and configuration data could not be used to generate a coherent pivot table."
}
}
}

/// A localized message describing the reason for the failure.
public var failureReason: String? {
get {
switch self {
case .noTable: return "The `table` variable was not set."
case .noFields: return "The `fields` variable was not set."
case .noOutput: return "The combination of input and configuration data was not coherent and a pivot table could not be generated. For example a `FieldName` or `FieldValue` may be used in `filters` or `fields` that is not present in the input `table`."
}
}}

/// A localized message describing how one might recover from the failure.
public var recoverySuggestion: String? {
get {
switch self {
case .noTable: return "Set the `table` variable to a non-nil value."
case .noFields: return "Set the `fields` variable to a non-nil value."
case .noOutput: return "Ensure that the combination of input variables (`table`, `fields`) and configuration variables (`sortDescriptors`, `filters`) is coherent and can be used to build a pivot. See the PeakPivot README for more information on how these variables work."
}
}}
}

public extension BuildPivot {
Expand Down
44 changes: 6 additions & 38 deletions Sources/PeakPivot/BuildPivotDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,17 @@ import Foundation
/// Describes how to sort a pivot table
public enum BuildPivotDescriptor: Equatable, CaseIterable {
case byTitle(ascending: Bool = true)
case byValue(ascending: Bool = true)
case byCount(ascending: Bool = true)
case bySum(ascending: Bool = true)

public static var allCases: [BuildPivotDescriptor] {
return [
.byTitle(ascending: true),
.byTitle(ascending: false),
.byValue(ascending: true),
.byValue(ascending: false)
.byCount(ascending: true),
.byCount(ascending: false),
.bySum(ascending: true),
.bySum(ascending: false)
]
}
}

// TODO: Fix this
//extension BuildPivotDescriptor: Codable {
// enum CodingKeys: String, CodingKey {
// case byTitle, byValue
// }
//
// public init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
// if let boolValue = try container.decodeIfPresent(Bool.self, forKey: .byTitle) {
// self = .byTitle(ascending: boolValue)
// }
// else if let boolValue = try container.decodeIfPresent(Bool.self, forKey: .byValue) {
// self = .byValue(ascending: boolValue)
// }
// else {
// throw BuildPivotDescriptorDecodingError.unknownKey
// }
//
// }
//
// public func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
// switch self {
// case .byTitle(let ascending):
// try container.encode(ascending, forKey: .byTitle)
// case .byValue(let ascending):
// try container.encode(ascending, forKey: .byValue)
// }
// }
//}
//
//public enum BuildPivotDescriptorDecodingError: Error {
// case unknownKey
//}
Loading

0 comments on commit be8fc1b

Please sign in to comment.