summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE18
-rw-r--r--data/invoices.csv4
-rw-r--r--data/settings.lua8
-rw-r--r--fonts/Roboto/LICENSE.txt202
-rw-r--r--fonts/Roboto/Roboto-Black.ttfbin0 -> 163488 bytes
-rw-r--r--fonts/Roboto/Roboto-BlackItalic.ttfbin0 -> 165444 bytes
-rw-r--r--fonts/Roboto/Roboto-Bold.ttfbin0 -> 162464 bytes
-rw-r--r--fonts/Roboto/Roboto-BoldItalic.ttfbin0 -> 163644 bytes
-rw-r--r--fonts/Roboto/Roboto-Italic.ttfbin0 -> 161484 bytes
-rw-r--r--fonts/Roboto/Roboto-Light.ttfbin0 -> 162420 bytes
-rw-r--r--fonts/Roboto/Roboto-LightItalic.ttfbin0 -> 166492 bytes
-rw-r--r--fonts/Roboto/Roboto-Medium.ttfbin0 -> 162588 bytes
-rw-r--r--fonts/Roboto/Roboto-MediumItalic.ttfbin0 -> 165636 bytes
-rw-r--r--fonts/Roboto/Roboto-Regular.ttfbin0 -> 162876 bytes
-rw-r--r--fonts/Roboto/Roboto-Thin.ttfbin0 -> 163132 bytes
-rw-r--r--fonts/Roboto/Roboto-ThinItalic.ttfbin0 -> 168276 bytes
-rw-r--r--laskuri.lua164
-rw-r--r--lua/ftcsv.lua490
-rw-r--r--lua/laskuri/refnum.lua22
-rw-r--r--lua/laskuri/utils.lua30
-rw-r--r--lua/pprint.lua455
21 files changed, 1393 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6e4ec28
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,18 @@
+Copyright 2017 Oskari Timperi
+
+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.
diff --git a/data/invoices.csv b/data/invoices.csv
new file mode 100644
index 0000000..7a9153f
--- /dev/null
+++ b/data/invoices.csv
@@ -0,0 +1,4 @@
+Customer,InvoiceNr,Product,Amount,Unit,Price,Vat
+Matti Mallikas,17000,Jäsenmaksu,1,kpl,20,0
+,17000,T-Paita,1,kpl,15,10
+Maija Mallikas,17001,Pikkujoulu,1,kpl,49,0
diff --git a/data/settings.lua b/data/settings.lua
new file mode 100644
index 0000000..0bd91ee
--- /dev/null
+++ b/data/settings.lua
@@ -0,0 +1,8 @@
+invoicer = ""
+address_line_1 = ""
+address_line_2 = ""
+business_id = ""
+www_address = ""
+iban = ""
+bic_swift = ""
+date_fmt = "%d.%m.%Y"
diff --git a/fonts/Roboto/LICENSE.txt b/fonts/Roboto/LICENSE.txt
new file mode 100644
index 0000000..75b5248
--- /dev/null
+++ b/fonts/Roboto/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/fonts/Roboto/Roboto-Black.ttf b/fonts/Roboto/Roboto-Black.ttf
new file mode 100644
index 0000000..fbde625
--- /dev/null
+++ b/fonts/Roboto/Roboto-Black.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-BlackItalic.ttf b/fonts/Roboto/Roboto-BlackItalic.ttf
new file mode 100644
index 0000000..60f7782
--- /dev/null
+++ b/fonts/Roboto/Roboto-BlackItalic.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Bold.ttf b/fonts/Roboto/Roboto-Bold.ttf
new file mode 100644
index 0000000..a355c27
--- /dev/null
+++ b/fonts/Roboto/Roboto-Bold.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-BoldItalic.ttf b/fonts/Roboto/Roboto-BoldItalic.ttf
new file mode 100644
index 0000000..3c9a7a3
--- /dev/null
+++ b/fonts/Roboto/Roboto-BoldItalic.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Italic.ttf b/fonts/Roboto/Roboto-Italic.ttf
new file mode 100644
index 0000000..ff6046d
--- /dev/null
+++ b/fonts/Roboto/Roboto-Italic.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Light.ttf b/fonts/Roboto/Roboto-Light.ttf
new file mode 100644
index 0000000..94c6bcc
--- /dev/null
+++ b/fonts/Roboto/Roboto-Light.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-LightItalic.ttf b/fonts/Roboto/Roboto-LightItalic.ttf
new file mode 100644
index 0000000..04cc002
--- /dev/null
+++ b/fonts/Roboto/Roboto-LightItalic.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Medium.ttf b/fonts/Roboto/Roboto-Medium.ttf
new file mode 100644
index 0000000..39c63d7
--- /dev/null
+++ b/fonts/Roboto/Roboto-Medium.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-MediumItalic.ttf b/fonts/Roboto/Roboto-MediumItalic.ttf
new file mode 100644
index 0000000..dc743f0
--- /dev/null
+++ b/fonts/Roboto/Roboto-MediumItalic.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Regular.ttf b/fonts/Roboto/Roboto-Regular.ttf
new file mode 100644
index 0000000..8c082c8
--- /dev/null
+++ b/fonts/Roboto/Roboto-Regular.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-Thin.ttf b/fonts/Roboto/Roboto-Thin.ttf
new file mode 100644
index 0000000..d695550
--- /dev/null
+++ b/fonts/Roboto/Roboto-Thin.ttf
Binary files differ
diff --git a/fonts/Roboto/Roboto-ThinItalic.ttf b/fonts/Roboto/Roboto-ThinItalic.ttf
new file mode 100644
index 0000000..07172ff
--- /dev/null
+++ b/fonts/Roboto/Roboto-ThinItalic.ttf
Binary files differ
diff --git a/laskuri.lua b/laskuri.lua
new file mode 100644
index 0000000..8fc2de1
--- /dev/null
+++ b/laskuri.lua
@@ -0,0 +1,164 @@
+local ftcsv = require("ftcsv")
+local refnum = require("laskuri.refnum")
+local utils = require("laskuri.utils")
+local pprint = require("pprint")
+
+dofile("settings.lua")
+
+-- Load data from the file `invoices.csv`
+local data = ftcsv.parse("invoices.csv", ",")
+
+-- Do some preprocessing for each row
+
+local num_fields = { "Amount", "Price", "Vat" }
+
+for _, row in pairs(data) do
+ -- Convert applicable fields to lua numbers
+ for _, field in pairs(num_fields) do
+ row[field] = tonumber(row[field])
+ end
+
+ -- Calculate tax and final price for each row
+ row.Tax = row.Price * (row.Vat / 100.0)
+ row.FinalPrice = row.Price + row.Tax
+end
+
+-- Group the data (rows) by the invoice number.
+local invoices = utils.group_by(data, function(invoice)
+ return invoice.InvoiceNr
+end)
+
+-- Calculate some needed data for each invoice including price, final price, due
+-- date and reference number
+for invoice_nr, rows in pairs(invoices) do
+ local price = 0.0
+ local final_price = 0.0
+ local tax = 0.0
+
+ for _, row in pairs(rows) do
+ price = price + row.Price
+ final_price = final_price + row.FinalPrice
+ tax = tax + row.Tax
+ end
+
+ rows.data = {
+ Price = price,
+ FinalPrice = final_price,
+ Tax = tax,
+ Due = utils.due_date(14, date_fmt),
+ ReferenceNum = refnum.refnum(invoice_nr),
+ }
+end
+
+local today = os.date(date_fmt)
+
+-- pprint.pprint(invoices)
+
+function to_filename(s)
+ return string.gsub(s, "[^%w]", "_")
+end
+
+for invoice_nr, invoice in pairs(invoices) do
+ doc = Document:New()
+
+ doc:SetCompressionMode("all")
+ doc:UseUTFEncodings()
+
+ page = doc:AddPage()
+
+ page:SetSize("A4", "portrait")
+
+ font = doc:LoadTTFontFromFile("fonts/Roboto/Roboto-Regular.ttf", true, "UTF-8")
+ font_bold = doc:LoadTTFontFromFile("fonts/Roboto/Roboto-Bold.ttf", true, "UTF-8")
+
+ page:CreateGrid(15, 50)
+ -- page:DrawGrid()
+
+ page:BeginText()
+
+ page:SetFontAndSize(font, 12)
+ page:TextCell(0, 0, 3, 1, invoicer, "left")
+
+ page:SetFontAndSize(font, 10)
+ page:TextCell(0, 1, 3, 1, address_line_1, "left")
+ page:TextCell(0, 2, 3, 1, address_line_2, "left")
+
+ page:SetFontAndSize(font_bold, 14)
+ page:TextCell(8, 0, 3, 1, "LASKU", "left")
+
+ page:SetFontAndSize(font, 9)
+ page:TextCell(8, 2, 3, 1, "Laskun päiväys:", "left")
+ page:TextCell(8, 3, 3, 1, today, "left")
+
+ page:TextCell(8, 4, 3, 1, "Laskunumero:", "left")
+ page:TextCell(8, 5, 3, 1, invoice_nr, "left")
+
+ page:TextCell(8, 6, 3, 1, "Eräpäivä:", "left")
+ page:TextCell(8, 7, 3, 1, invoice.data.Due, "left")
+
+ page:SetFontAndSize(font_bold, 10)
+ page:TextCell(0, 5, 3, 1, invoice[1].Customer, "left")
+
+ page:SetFontAndSize(font, 10)
+ page:TextCell(0, 10, 3, 1, "Nimike", "left")
+ page:TextCell(3, 10, 2, 1, "Määrä", "right")
+ page:TextCell(5, 10, 2, 1, "Yks.", "left")
+ page:TextCell(7, 10, 2, 1, "A'hinta EUR", "right")
+ page:TextCell(9, 10, 2, 1, "Alv %", "right")
+ page:TextCell(11, 10, 3, 1, "Verollinen yht. EUR", "right")
+
+ product_row = 11
+
+ for idx = 1, #invoice do
+ product = invoice[idx]
+ local row = product_row + idx - 1
+ page:TextCell(0, row, 3, 1, product.Product, "left")
+ page:TextCell(3, row, 2, 1, string.format("%d", product.Amount), "right")
+ page:TextCell(5, row, 2, 1, product.Unit, "left")
+ page:TextCell(7, row, 2, 1, string.format("%.2f", product.Price), "right")
+ page:TextCell(9, row, 2, 1, string.format("%d", product.Vat), "right")
+ page:TextCell(11, row, 3, 1, string.format("%.2f", product.FinalPrice), "right")
+ end
+
+ page:SetFontAndSize(font_bold, 10)
+ page:TextCell(7, 38, 4, 1, "Veroton yhteensä EUR", "right")
+ page:TextCell(7, 39, 4, 1, "ALV yhteensä EUR", "right")
+ page:TextCell(7, 40, 4, 1, "Verollinen yhteensä EUR", "right")
+
+ page:SetFontAndSize(font, 10)
+ page:TextCell(11, 38, 3, 1, string.format("%.2f", invoice.data.Price), "right")
+ page:TextCell(11, 39, 3, 1, string.format("%.2f", invoice.data.Tax), "right")
+ page:TextCell(11, 40, 3, 1, string.format("%.2f", invoice.data.FinalPrice), "right")
+
+ page:TextCell(0, 43, 7, 1, "IBAN:", "left")
+ page:TextCell(0, 44, 7, 1, iban, "left")
+ page:TextCell(7, 43, 4, 1, "BIC / SWIFT:", "left")
+ page:TextCell(7, 44, 4, 1, bic_swift, "left")
+ page:TextCell(11, 43, 3, 1, "Eräpäivä:", "left")
+ page:TextCell(11, 44, 3, 1, invoice.data.Due, "left")
+
+ page:TextCell(0, 45, 7, 1, "Viitenumero:", "left")
+ page:SetFontAndSize(font_bold, 10)
+ page:TextCell(0, 46, 7, 1, invoice.data.ReferenceNum, "left")
+ page:SetFontAndSize(font, 10)
+ page:TextCell(7, 45, 7, 1, "Yhteensä EUR:", "left")
+ page:SetFontAndSize(font_bold, 10)
+ page:TextCell(7, 46, 7, 1, string.format("%.2f", invoice.data.FinalPrice), "left")
+
+ page:SetFontAndSize(font, 10)
+ page:TextCell(0, 47, 7, 1, invoicer, "left")
+ page:TextCell(0, 48, 7, 1, address_line_1, "left")
+ page:TextCell(0, 49, 7, 1, address_line_2, "left")
+
+ page:TextCell(7, 47, 2, 1, "Y-tunnus:", "left")
+ page:TextCell(9, 47, 5, 1, business_id, "left")
+
+ page:TextCell(7, 48, 2, 1, "WWW:", "left")
+ page:TextCell(9, 48, 5, 1, www_address, "left")
+
+ page:EndText()
+
+ filename = to_filename(invoice_nr .. " " .. invoice[1].Customer)
+
+ doc:Save(filename .. ".pdf")
+end
diff --git a/lua/ftcsv.lua b/lua/ftcsv.lua
new file mode 100644
index 0000000..af4abec
--- /dev/null
+++ b/lua/ftcsv.lua
@@ -0,0 +1,490 @@
+local ftcsv = {
+ _VERSION = 'ftcsv 1.1.3',
+ _DESCRIPTION = 'CSV library for Lua',
+ _URL = 'https://github.com/FourierTransformer/ftcsv',
+ _LICENSE = [[
+ The MIT License (MIT)
+
+ Copyright (c) 2016 Shakil Thakur
+
+ 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.
+ ]]
+}
+
+-- lua 5.1 load compat
+local M = {}
+if type(jit) == 'table' or _ENV then
+ M.load = _G.load
+else
+ M.load = loadstring
+end
+
+-- perf
+local sbyte = string.byte
+local ssub = string.sub
+
+-- luajit specific speedups
+-- luajit performs faster with iterating over string.byte,
+-- whereas vanilla lua performs faster with string.find
+if type(jit) == 'table' then
+ -- finds the end of an escape sequence
+ function M.findClosingQuote(i, inputLength, inputString, quote, doubleQuoteEscape)
+ -- local doubleQuoteEscape = doubleQuoteEscape
+ local currentChar, nextChar = sbyte(inputString, i), nil
+ while i <= inputLength do
+ -- print(i)
+ nextChar = sbyte(inputString, i+1)
+
+ -- this one deals with " double quotes that are escaped "" within single quotes "
+ -- these should be turned into a single quote at the end of the field
+ if currentChar == quote and nextChar == quote then
+ doubleQuoteEscape = true
+ i = i + 2
+ currentChar = sbyte(inputString, i)
+
+ -- identifies the escape toggle
+ elseif currentChar == quote and nextChar ~= quote then
+ -- print("exiting", i-1)
+ return i-1, doubleQuoteEscape
+ else
+ i = i + 1
+ currentChar = nextChar
+ end
+ end
+ end
+
+else
+ -- vanilla lua closing quote finder
+ function M.findClosingQuote(i, inputLength, inputString, quote, doubleQuoteEscape)
+ local firstCharIndex = 1
+ local firstChar, iChar = nil, nil
+ repeat
+ firstCharIndex, i = inputString:find('".?', i+1)
+ firstChar = sbyte(inputString, firstCharIndex)
+ iChar = sbyte(inputString, i)
+ -- nextChar = string.byte(inputString, i+1)
+ -- print("HI", offset, i)
+ -- print(firstChar, iChar)
+ if firstChar == quote and iChar == quote then
+ doubleQuoteEscape = true
+ end
+ until iChar ~= quote
+ if i == nil then
+ return inputLength-1, doubleQuoteEscape
+ end
+ -- print("exiting", i-2)
+ return i-2, doubleQuoteEscape
+ end
+
+end
+
+-- load an entire file into memory
+local function loadFile(textFile)
+ local file = io.open(textFile, "r")
+ if not file then error("ftcsv: File not found at " .. textFile) end
+ local allLines = file:read("*all")
+ file:close()
+ return allLines
+end
+
+-- creates a new field and adds it to the main table
+local function createField(inputString, quote, fieldStart, i, doubleQuoteEscape)
+ local field
+ -- so, if we just recently de-escaped, we don't want the trailing \"
+ if sbyte(inputString, i-1) == quote then
+ -- print("Skipping last \"")
+ field = ssub(inputString, fieldStart, i-2)
+ else
+ field = ssub(inputString, fieldStart, i-1)
+ end
+ if doubleQuoteEscape then
+ -- print("QUOTE REPLACE")
+ -- print(line[fieldNum])
+ field = field:gsub('""', '"')
+ end
+ return field
+end
+
+-- main function used to parse
+local function parseString(inputString, inputLength, delimiter, i, headerField, fieldsToKeep)
+
+ -- keep track of my chars!
+ local currentChar, nextChar = sbyte(inputString, i), nil
+ local skipChar = 0
+ local field
+ local fieldStart = i
+ local fieldNum = 1
+ local lineNum = 1
+ local doubleQuoteEscape = false
+ local exit = false
+
+ --bytes
+ local CR = sbyte("\r")
+ local LF = sbyte("\n")
+ local quote = sbyte('"')
+ local delimiterByte = sbyte(delimiter)
+
+ local assignValue
+ local outResults
+ -- the headers haven't been set yet.
+ -- aka this is the first run!
+ if headerField == nil then
+ -- print("this is for headers")
+ headerField = {}
+ assignValue = function()
+ headerField[fieldNum] = field
+ return true
+ end
+ else
+ -- print("this is for magic")
+ outResults = {}
+ outResults[1] = {}
+ assignValue = function()
+ if not pcall(function()
+ outResults[lineNum][headerField[fieldNum]] = field
+ end) then
+ error('ftcsv: too many columns in row ' .. lineNum)
+ end
+ end
+ end
+
+ -- calculate the initial line count (note: this can include duplicates)
+ local headerFieldsExist = {}
+ local initialLineCount = 0
+ for _, value in pairs(headerField) do
+ if not headerFieldsExist[value] and (fieldsToKeep == nil or fieldsToKeep[value]) then
+ headerFieldsExist[value] = true
+ initialLineCount = initialLineCount + 1
+ end
+ end
+
+ while i <= inputLength do
+ -- go by two chars at a time! currentChar is set at the bottom.
+ -- currentChar = string.byte(inputString, i)
+ nextChar = sbyte(inputString, i+1)
+ -- print(i, string.char(currentChar), string.char(nextChar))
+
+ -- empty string
+ if currentChar == quote and nextChar == quote then
+ -- print("EMPTY STRING")
+ skipChar = 1
+ fieldStart = i + 2
+ -- print("fs+2:", fieldStart)
+
+ -- identifies the escape toggle.
+ -- This can only happen if fields have quotes around them
+ -- so the current "start" has to be where a quote character is.
+ elseif currentChar == quote and nextChar ~= quote and fieldStart == i then
+ -- print("New Quoted Field", i)
+ fieldStart = i + 1
+ i, doubleQuoteEscape = M.findClosingQuote(i+1, inputLength, inputString, quote, doubleQuoteEscape)
+ -- print("I VALUE", i, doubleQuoteEscape)
+ skipChar = 1
+
+ -- create some fields if we can!
+ elseif currentChar == delimiterByte then
+ -- create the new field
+ -- print(headerField[fieldNum])
+ if fieldsToKeep == nil or fieldsToKeep[headerField[fieldNum]] then
+ field = createField(inputString, quote, fieldStart, i, doubleQuoteEscape)
+ -- print("FIELD", field, "FIELDEND", headerField[fieldNum], lineNum)
+ -- outResults[headerField[fieldNum]][lineNum] = field
+ assignValue()
+ end
+ doubleQuoteEscape = false
+
+ fieldNum = fieldNum + 1
+ fieldStart = i + 1
+ -- print("fs+1:", fieldStart)
+ -- end
+
+ -- newline?!
+ elseif ((currentChar == CR and nextChar == LF) or currentChar == LF) then
+ if fieldsToKeep == nil or fieldsToKeep[headerField[fieldNum]] then
+ -- create the new field
+ field = createField(inputString, quote, fieldStart, i, doubleQuoteEscape)
+
+ -- outResults[headerField[fieldNum]][lineNum] = field
+ exit = assignValue()
+ if exit then
+ if (currentChar == CR and nextChar == LF) then
+ return headerField, i + 1
+ else
+ return headerField, i
+ end
+ end
+ end
+ doubleQuoteEscape = false
+
+ -- determine how line ends
+ if (currentChar == CR and nextChar == LF) then
+ -- print("CRLF DETECTED")
+ skipChar = 1
+ fieldStart = fieldStart + 1
+ -- print("fs:", fieldStart)
+ end
+
+ -- incrememnt for new line
+ if fieldNum < initialLineCount then
+ error('ftcsv: too few columns in row ' .. lineNum)
+ end
+ lineNum = lineNum + 1
+ outResults[lineNum] = {}
+ fieldNum = 1
+ fieldStart = i + 1 + skipChar
+ -- print("fs:", fieldStart)
+
+ end
+
+ i = i + 1 + skipChar
+ if (skipChar > 0) then
+ currentChar = sbyte(inputString, i)
+ else
+ currentChar = nextChar
+ end
+ skipChar = 0
+ end
+
+ -- create last new field
+ if fieldsToKeep == nil or fieldsToKeep[headerField[fieldNum]] then
+ field = createField(inputString, quote, fieldStart, i, doubleQuoteEscape)
+ assignValue()
+ end
+
+ -- clean up last line if it's weird (this happens when there is a CRLF newline at end of file)
+ -- doing a count gets it to pick up the oddballs
+ local finalLineCount = 0
+ local lastValue = nil
+ for k, v in pairs(outResults[lineNum]) do
+ finalLineCount = finalLineCount + 1
+ lastValue = v
+ end
+
+ -- this indicates a CRLF
+ -- print("Final/Initial", finalLineCount, initialLineCount)
+ if finalLineCount == 1 and lastValue == "" then
+ outResults[lineNum] = nil
+
+ -- otherwise there might not be enough line
+ elseif finalLineCount < initialLineCount then
+ error('ftcsv: too few columns in row ' .. lineNum)
+ end
+
+ return outResults
+end
+
+-- runs the show!
+function ftcsv.parse(inputFile, delimiter, options)
+ -- delimiter MUST be one character
+ assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
+
+ -- OPTIONS yo
+ local header = true
+ local rename
+ local fieldsToKeep = nil
+ local loadFromString = false
+ local headerFunc
+ if options then
+ if options.headers ~= nil then
+ assert(type(options.headers) == "boolean", "ftcsv only takes the boolean 'true' or 'false' for the optional parameter 'headers' (default 'true'). You passed in '" .. tostring(options.headers) .. "' of type '" .. type(options.headers) .. "'.")
+ header = options.headers
+ end
+ if options.rename ~= nil then
+ assert(type(options.rename) == "table", "ftcsv only takes in a key-value table for the optional parameter 'rename'. You passed in '" .. tostring(options.rename) .. "' of type '" .. type(options.rename) .. "'.")
+ rename = options.rename
+ end
+ if options.fieldsToKeep ~= nil then
+ assert(type(options.fieldsToKeep) == "table", "ftcsv only takes in a list (as a table) for the optional parameter 'fieldsToKeep'. You passed in '" .. tostring(options.fieldsToKeep) .. "' of type '" .. type(options.fieldsToKeep) .. "'.")
+ local ofieldsToKeep = options.fieldsToKeep
+ if ofieldsToKeep ~= nil then
+ fieldsToKeep = {}
+ for j = 1, #ofieldsToKeep do
+ fieldsToKeep[ofieldsToKeep[j]] = true
+ end
+ end
+ if header == false and options.rename == nil then
+ error("ftcsv: fieldsToKeep only works with header-less files when using the 'rename' functionality")
+ end
+ end
+ if options.loadFromString ~= nil then
+ assert(type(options.loadFromString) == "boolean", "ftcsv only takes a boolean value for optional parameter 'loadFromString'. You passed in '" .. tostring(options.loadFromString) .. "' of type '" .. type(options.loadFromString) .. "'.")
+ loadFromString = options.loadFromString
+ end
+ if options.headerFunc ~= nil then
+ assert(type(options.headerFunc) == "function", "ftcsv only takes a function value for optional parameter 'headerFunc'. You passed in '" .. tostring(options.headerFunc) .. "' of type '" .. type(options.headerFunc) .. "'.")
+ headerFunc = options.headerFunc
+ end
+ end
+
+ -- handle input via string or file!
+ local inputString
+ if loadFromString then
+ inputString = inputFile
+ else
+ inputString = loadFile(inputFile)
+ end
+ local inputLength = #inputString
+
+ -- if they sent in an empty file...
+ if inputLength == 0 then
+ error('ftcsv: Cannot parse an empty file')
+ end
+
+ -- parse through the headers!
+ local headerField, i = parseString(inputString, inputLength, delimiter, 1)
+ i = i + 1 -- start at the next char
+
+ -- make sure a header isn't empty
+ for _, header in ipairs(headerField) do
+ if #header == 0 then
+ error('ftcsv: Cannot parse a file which contains empty headers')
+ end
+ end
+
+ -- for files where there aren't headers!
+ if header == false then
+ i = 0
+ for j = 1, #headerField do
+ headerField[j] = j
+ end
+ end
+
+ -- rename fields as needed!
+ if rename then
+ -- basic rename (["a" = "apple"])
+ for j = 1, #headerField do
+ if rename[headerField[j]] then
+ -- print("RENAMING", headerField[j], rename[headerField[j]])
+ headerField[j] = rename[headerField[j]]
+ end
+ end
+ -- files without headers, but with a rename need to be handled too!
+ if #rename > 0 then
+ for j = 1, #rename do
+ headerField[j] = rename[j]
+ end
+ end
+ end
+
+ -- apply some sweet header manipulation
+ if headerFunc then
+ for j = 1, #headerField do
+ headerField[j] = headerFunc(headerField[j])
+ end
+ end
+
+ local output = parseString(inputString, inputLength, delimiter, i, headerField, fieldsToKeep)
+ return output
+end
+
+-- a function that delimits " to "", used by the writer
+local function delimitField(field)
+ field = tostring(field)
+ if field:find('"') then
+ return field:gsub('"', '""')
+ else
+ return field
+ end
+end
+
+-- a function that compiles some lua code to quickly print out the csv
+local function writer(inputTable, dilimeter, headers)
+ -- they get re-created here if they need to be escaped so lua understands it based on how
+ -- they came in
+ for i = 1, #headers do
+ if inputTable[1][headers[i]] == nil then
+ error("ftcsv: the field '" .. headers[i] .. "' doesn't exist in the inputTable")
+ end
+ if headers[i]:find('"') then
+ headers[i] = headers[i]:gsub('"', '\\"')
+ end
+ end
+
+ local outputFunc = [[
+ local state, i = ...
+ local d = state.delimitField
+ i = i + 1;
+ if i > state.tableSize then return nil end;
+ return i, '"' .. d(state.t[i]["]] .. table.concat(headers, [["]) .. '"]] .. dilimeter .. [["' .. d(state.t[i]["]]) .. [["]) .. '"\r\n']]
+
+ -- print(outputFunc)
+
+ local state = {}
+ state.t = inputTable
+ state.tableSize = #inputTable
+ state.delimitField = delimitField
+
+ return M.load(outputFunc), state, 0
+
+end
+
+-- takes the values from the headers in the first row of the input table
+local function extractHeaders(inputTable)
+ local headers = {}
+ for key, _ in pairs(inputTable[1]) do
+ headers[#headers+1] = key
+ end
+
+ -- lets make the headers alphabetical
+ table.sort(headers)
+
+ return headers
+end
+
+-- turns a lua table into a csv
+-- works really quickly with luajit-2.1, because table.concat life
+function ftcsv.encode(inputTable, delimiter, options)
+ local output = {}
+
+ -- dilimeter MUST be one character
+ assert(#delimiter == 1 and type(delimiter) == "string", "the delimiter must be of string type and exactly one character")
+
+ -- grab the headers from the options if they are there
+ local headers = nil
+ if options then
+ if options.fieldsToKeep ~= nil then
+ assert(type(options.fieldsToKeep) == "table", "ftcsv only takes in a list (as a table) for the optional parameter 'fieldsToKeep'. You passed in '" .. tostring(options.headers) .. "' of type '" .. type(options.headers) .. "'.")
+ headers = options.fieldsToKeep
+ end
+ end
+ if headers == nil then
+ headers = extractHeaders(inputTable)
+ end
+
+ -- newHeaders are needed if there are quotes within the header
+ -- because they need to be escaped
+ local newHeaders = {}
+ for i = 1, #headers do
+ if headers[i]:find('"') then
+ newHeaders[i] = headers[i]:gsub('"', '""')
+ else
+ newHeaders[i] = headers[i]
+ end
+ end
+ output[1] = '"' .. table.concat(newHeaders, '"' .. delimiter .. '"') .. '"\r\n'
+
+ -- add each line by line.
+ for i, line in writer(inputTable, delimiter, headers) do
+ output[i+1] = line
+ end
+ return table.concat(output)
+end
+
+return ftcsv
+
diff --git a/lua/laskuri/refnum.lua b/lua/laskuri/refnum.lua
new file mode 100644
index 0000000..ecd83de
--- /dev/null
+++ b/lua/laskuri/refnum.lua
@@ -0,0 +1,22 @@
+-- Calculate the Finnish reference number
+local refnum = function(invoicenr)
+ local reversed = string.reverse(invoicenr)
+ local weights = {7, 3, 1}
+ local counter = 0
+ local sum = 0
+ for digit in string.gmatch(reversed, "%d") do
+ local n = tonumber(digit)
+ local w = weights[(counter % 3) + 1]
+ counter = counter + 1
+ sum = sum + n * w
+ end
+ local check = (sum + (10 - (sum % 10))) - sum
+ if check == 10 then
+ check = 0
+ end
+ return invoicenr .. tostring(math.tointeger(check))
+end
+
+return {
+ refnum = refnum
+}
diff --git a/lua/laskuri/utils.lua b/lua/laskuri/utils.lua
new file mode 100644
index 0000000..76db2ad
--- /dev/null
+++ b/lua/laskuri/utils.lua
@@ -0,0 +1,30 @@
+-- Group values from given table by the function `fun`. Values from the table
+-- are passed to `fun` and `fun` should return some value by which the values
+-- are grouped by.
+local group_by = function(tbl, fun)
+ local result = {}
+ for k, v in pairs(tbl) do
+ local k = fun(v)
+ if not result[k] then
+ result[k] = {}
+ result[k][1] = v
+ else
+ result[k][#result[k]+1] = v
+ end
+ end
+ return result
+end
+
+-- Calculate the due date from this day forwards.
+local due_date = function(days, fmt)
+ fmt = fmt or "%Y-%m-%d"
+ local days_secs = 60 * 60 * 24 * days
+ local now = os.time()
+ local due = now + days_secs
+ return os.date(fmt, due)
+end
+
+return {
+ group_by = group_by,
+ due_date = due_date
+}
diff --git a/lua/pprint.lua b/lua/pprint.lua
new file mode 100644
index 0000000..7b1297d
--- /dev/null
+++ b/lua/pprint.lua
@@ -0,0 +1,455 @@
+local pprint = { VERSION = '0.1' }
+
+pprint.defaults = {
+ -- type display trigger, hide not useful datatypes by default
+ -- custom types are treated as table
+ show_nil = true,
+ show_boolean = true,
+ show_number = true,
+ show_string = true,
+ show_table = true,
+ show_function = false,
+ show_thread = false,
+ show_userdata = false,
+ -- additional display trigger
+ show_metatable = false, -- show metatable
+ show_all = false, -- override other show settings and show everything
+ use_tostring = false, -- use __tostring to print table if available
+ filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide
+ object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache
+ -- per process, falsy value to disable (might cause infinite loop)
+ -- format settings
+ indent_size = 2, -- indent for each nested table level
+ level_width = 80, -- max width per indent level
+ wrap_string = true, -- wrap string when it's longer than level_width
+ wrap_array = false, -- wrap every array elements
+ sort_keys = true, -- sort table keys
+}
+
+local TYPES = {
+ ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4,
+ ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8
+}
+
+-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a'
+local ESCAPE_MAP = {
+ ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r',
+ ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\',
+}
+
+-- generic utilities
+local function escape(s)
+ s = s:gsub('([%c\\])', ESCAPE_MAP)
+ local dq = s:find('"')
+ local sq = s:find("'")
+ if dq and sq then
+ return s:gsub('"', '\\"'), '"'
+ elseif sq then
+ return s, '"'
+ else
+ return s, "'"
+ end
+end
+
+local function is_plain_key(key)
+ return type(key) == 'string' and key:match('^[%a_][%a%d_]*$')
+end
+
+local CACHE_TYPES = {
+ ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true
+}
+
+-- cache would be populated to be like:
+-- {
+-- function = { `fun1` = 1, _cnt = 1 }, -- object id
+-- table = { `table1` = 1, `table2` = 2, _cnt = 2 },
+-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count
+-- }
+-- use weakrefs to avoid accidentall adding refcount
+local function cache_apperance(obj, cache, option)
+ if not cache.visited_tables then
+ cache.visited_tables = setmetatable({}, {__mode = 'k'})
+ end
+ local t = type(obj)
+
+ -- TODO can't test filter_function here as we don't have the ix and key,
+ -- might cause different results?
+ -- respect show_xxx and filter_function to be consistent with print results
+ if (not TYPES[t] and not option.show_table)
+ or (TYPES[t] and not option['show_'..t]) then
+ return
+ end
+
+ if CACHE_TYPES[t] or TYPES[t] == nil then
+ if not cache[t] then
+ cache[t] = setmetatable({}, {__mode = 'k'})
+ cache[t]._cnt = 0
+ end
+ if not cache[t][obj] then
+ cache[t]._cnt = cache[t]._cnt + 1
+ cache[t][obj] = cache[t]._cnt
+ end
+ end
+ if t == 'table' or TYPES[t] == nil then
+ if cache.visited_tables[obj] == false then
+ -- already printed, no need to mark this and its children anymore
+ return
+ elseif cache.visited_tables[obj] == nil then
+ cache.visited_tables[obj] = 1
+ else
+ -- visited already, increment and continue
+ cache.visited_tables[obj] = cache.visited_tables[obj] + 1
+ return
+ end
+ for k, v in pairs(obj) do
+ cache_apperance(k, cache, option)
+ cache_apperance(v, cache, option)
+ end
+ local mt = getmetatable(obj)
+ if mt and option.show_metatable then
+ cache_apperance(mt, cache, option)
+ end
+ end
+end
+
+-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method
+local function str_natural_cmp(lhs, rhs)
+ while #lhs > 0 and #rhs > 0 do
+ local lmid, lend = lhs:find('%d+')
+ local rmid, rend = rhs:find('%d+')
+ if not (lmid and rmid) then return lhs < rhs end
+
+ local lsub = lhs:sub(1, lmid-1)
+ local rsub = rhs:sub(1, rmid-1)
+ if lsub ~= rsub then
+ return lsub < rsub
+ end
+
+ local lnum = tonumber(lhs:sub(lmid, lend))
+ local rnum = tonumber(rhs:sub(rmid, rend))
+ if lnum ~= rnum then
+ return lnum < rnum
+ end
+
+ lhs = lhs:sub(lend+1)
+ rhs = rhs:sub(rend+1)
+ end
+ return lhs < rhs
+end
+
+local function cmp(lhs, rhs)
+ local tleft = type(lhs)
+ local tright = type(rhs)
+ if tleft == 'number' and tright == 'number' then return lhs < rhs end
+ if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end
+ if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end
+
+ -- allow custom types
+ local oleft = TYPES[tleft] or 9
+ local oright = TYPES[tright] or 9
+ return oleft < oright
+end
+
+-- setup option with default
+local function make_option(option)
+ if option == nil then
+ option = {}
+ end
+ for k, v in pairs(pprint.defaults) do
+ if option[k] == nil then
+ option[k] = v
+ end
+ if option.show_all then
+ for t, _ in pairs(TYPES) do
+ option['show_'..t] = true
+ end
+ option.show_metatable = true
+ end
+ end
+ return option
+end
+
+-- override defaults and take effects for all following calls
+function pprint.setup(option)
+ pprint.defaults = make_option(option)
+end
+
+-- format lua object into a string
+function pprint.pformat(obj, option, printer)
+ option = make_option(option)
+ local buf = {}
+ local function default_printer(s)
+ table.insert(buf, s)
+ end
+ printer = printer or default_printer
+
+ local cache
+ if option.object_cache == 'global' then
+ -- steal the cache into a local var so it's not visible from _G or anywhere
+ -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway
+ cache = pprint._cache or {}
+ pprint._cache = nil
+ elseif option.object_cache == 'local' then
+ cache = {}
+ end
+
+ local last = '' -- used for look back and remove trailing comma
+ local status = {
+ indent = '', -- current indent
+ len = 0, -- current line length
+ }
+
+ local wrapped_printer = function(s)
+ printer(last)
+ last = s
+ end
+
+ local function _indent(d)
+ status.indent = string.rep(' ', d + #(status.indent))
+ end
+
+ local function _n(d)
+ wrapped_printer('\n')
+ wrapped_printer(status.indent)
+ if d then
+ _indent(d)
+ end
+ status.len = 0
+ return true -- used to close bracket correctly
+ end
+
+ local function _p(s, nowrap)
+ status.len = status.len + #s
+ if not nowrap and status.len > option.level_width then
+ _n()
+ wrapped_printer(s)
+ status.len = #s
+ else
+ wrapped_printer(s)
+ end
+ end
+
+ local formatter = {}
+ local function format(v)
+ local f = formatter[type(v)]
+ f = f or formatter.table -- allow patched type()
+ if option.filter_function and option.filter_function(v, nil, nil) then
+ return ''
+ else
+ return f(v)
+ end
+ end
+
+ local function tostring_formatter(v)
+ return tostring(v)
+ end
+
+ local function number_formatter(n)
+ return n == math.huge and '[[math.huge]]' or tostring(n)
+ end
+
+ local function nop_formatter(v)
+ return ''
+ end
+
+ local function make_fixed_formatter(t, has_cache)
+ if has_cache then
+ return function (v)
+ return string.format('[[%s %d]]', t, cache[t][v])
+ end
+ else
+ return function (v)
+ return '[['..t..']]'
+ end
+ end
+ end
+
+ local function string_formatter(s, force_long_quote)
+ local s, quote = escape(s)
+ local quote_len = force_long_quote and 4 or 2
+ if quote_len + #s + status.len > option.level_width then
+ _n()
+ -- only wrap string when is longer than level_width
+ if option.wrap_string and #s + quote_len > option.level_width then
+ -- keep the quotes together
+ _p('[[')
+ while #s + status.len >= option.level_width do
+ local seg = option.level_width - status.len
+ _p(string.sub(s, 1, seg), true)
+ _n()
+ s = string.sub(s, seg+1)
+ end
+ _p(s) -- print the remaining parts
+ return ']]'
+ end
+ end
+
+ return force_long_quote and '[['..s..']]' or quote..s..quote
+ end
+
+ local function table_formatter(t)
+ if option.use_tostring then
+ local mt = getmetatable(t)
+ if mt and mt.__tostring then
+ return string_formatter(tostring(t), true)
+ end
+ end
+
+ local print_header_ix = nil
+ local ttype = type(t)
+ if option.object_cache then
+ local cache_state = cache.visited_tables[t]
+ local tix = cache[ttype][t]
+ -- FIXME should really handle `cache_state == nil`
+ -- as user might add things through filter_function
+ if cache_state == false then
+ -- already printed, just print the the number
+ return string_formatter(string.format('%s %d', ttype, tix), true)
+ elseif cache_state > 1 then
+ -- appeared more than once, print table header with number
+ print_header_ix = tix
+ cache.visited_tables[t] = false
+ else
+ -- appeared exactly once, print like a normal table
+ end
+ end
+
+ local tlen = #t
+ local wrapped = false
+ _p('{')
+ _indent(option.indent_size)
+ _p(string.rep(' ', option.indent_size - 1))
+ if print_header_ix then
+ _p(string.format('--[[%s %d]] ', ttype, print_header_ix))
+ end
+ for ix = 1,tlen do
+ local v = t[ix]
+ if formatter[type(v)] == nop_formatter or
+ (option.filter_function and option.filter_function(v, ix, t)) then
+ -- pass
+ else
+ if option.wrap_array then
+ wrapped = _n()
+ end
+ _p(format(v)..', ')
+ end
+ end
+
+ -- hashmap part of the table, in contrast to array part
+ local function is_hash_key(k)
+ local numkey = tonumber(k)
+ if numkey ~= k or numkey > tlen then
+ return true
+ end
+ end
+
+ local function print_kv(k, v, t)
+ -- can't use option.show_x as obj may contain custom type
+ if formatter[type(v)] == nop_formatter or
+ formatter[type(k)] == nop_formatter or
+ (option.filter_function and option.filter_function(v, k, t)) then
+ return
+ end
+ wrapped = _n()
+ if is_plain_key(k) then
+ _p(k, true)
+ else
+ _p('[')
+ -- [[]] type string in key is illegal, needs to add spaces inbetween
+ local k = format(k)
+ if string.match(k, '%[%[') then
+ _p(' '..k..' ', true)
+ else
+ _p(k, true)
+ end
+ _p(']')
+ end
+ _p(' = ', true)
+ _p(format(v), true)
+ _p(',', true)
+ end
+
+ if option.sort_keys then
+ local keys = {}
+ for k, _ in pairs(t) do
+ if is_hash_key(k) then
+ table.insert(keys, k)
+ end
+ end
+ table.sort(keys, cmp)
+ for _, k in ipairs(keys) do
+ print_kv(k, t[k], t)
+ end
+ else
+ for k, v in pairs(t) do
+ if is_hash_key(k) then
+ print_kv(k, v, t)
+ end
+ end
+ end
+
+ if option.show_metatable then
+ local mt = getmetatable(t)
+ if mt then
+ print_kv('__metatable', mt, t)
+ end
+ end
+
+ _indent(-option.indent_size)
+ -- make { } into {}
+ last = string.gsub(last, '^ +$', '')
+ -- peek last to remove trailing comma
+ last = string.gsub(last, ',%s*$', ' ')
+ if wrapped then
+ _n()
+ end
+ _p('}')
+
+ return ''
+ end
+
+ -- set formatters
+ formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter
+ formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter
+ formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge
+ formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter
+ formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter
+ formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter
+ formatter['string'] = option.show_string and string_formatter or nop_formatter
+ formatter['table'] = option.show_table and table_formatter or nop_formatter
+
+ if option.object_cache then
+ -- needs to visit the table before start printing
+ cache_apperance(obj, cache, option)
+ end
+
+ _p(format(obj))
+ printer(last) -- close the buffered one
+
+ -- put cache back if global
+ if option.object_cache == 'global' then
+ pprint._cache = cache
+ end
+
+ return table.concat(buf)
+end
+
+-- pprint all the arguments
+function pprint.pprint( ... )
+ local args = {...}
+ -- select will get an accurate count of array len, counting trailing nils
+ local len = select('#', ...)
+ for ix = 1,len do
+ pprint.pformat(args[ix], nil, io.write)
+ io.write('\n')
+ end
+end
+
+setmetatable(pprint, {
+ __call = function (_, ...)
+ pprint.pprint(...)
+ end
+})
+
+return pprint
+