diff options
| author | Oskari Timperi <oskari.timperi@iki.fi> | 2017-04-10 20:28:02 +0300 |
|---|---|---|
| committer | Oskari Timperi <oskari.timperi@iki.fi> | 2017-04-10 20:28:02 +0300 |
| commit | 95ce3fc3f5a157a648b29a39c845a75be2a908b1 (patch) | |
| tree | 0a81a092c74022b2a1b6c77573e12b1ab403a470 | |
| download | laskuri-95ce3fc3f5a157a648b29a39c845a75be2a908b1.tar.gz laskuri-95ce3fc3f5a157a648b29a39c845a75be2a908b1.zip | |
Initial commit
21 files changed, 1393 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..fbde625 --- /dev/null +++ b/fonts/Roboto/Roboto-Black.ttf diff --git a/fonts/Roboto/Roboto-BlackItalic.ttf b/fonts/Roboto/Roboto-BlackItalic.ttf Binary files differnew file mode 100644 index 0000000..60f7782 --- /dev/null +++ b/fonts/Roboto/Roboto-BlackItalic.ttf diff --git a/fonts/Roboto/Roboto-Bold.ttf b/fonts/Roboto/Roboto-Bold.ttf Binary files differnew file mode 100644 index 0000000..a355c27 --- /dev/null +++ b/fonts/Roboto/Roboto-Bold.ttf diff --git a/fonts/Roboto/Roboto-BoldItalic.ttf b/fonts/Roboto/Roboto-BoldItalic.ttf Binary files differnew file mode 100644 index 0000000..3c9a7a3 --- /dev/null +++ b/fonts/Roboto/Roboto-BoldItalic.ttf diff --git a/fonts/Roboto/Roboto-Italic.ttf b/fonts/Roboto/Roboto-Italic.ttf Binary files differnew file mode 100644 index 0000000..ff6046d --- /dev/null +++ b/fonts/Roboto/Roboto-Italic.ttf diff --git a/fonts/Roboto/Roboto-Light.ttf b/fonts/Roboto/Roboto-Light.ttf Binary files differnew file mode 100644 index 0000000..94c6bcc --- /dev/null +++ b/fonts/Roboto/Roboto-Light.ttf diff --git a/fonts/Roboto/Roboto-LightItalic.ttf b/fonts/Roboto/Roboto-LightItalic.ttf Binary files differnew file mode 100644 index 0000000..04cc002 --- /dev/null +++ b/fonts/Roboto/Roboto-LightItalic.ttf diff --git a/fonts/Roboto/Roboto-Medium.ttf b/fonts/Roboto/Roboto-Medium.ttf Binary files differnew file mode 100644 index 0000000..39c63d7 --- /dev/null +++ b/fonts/Roboto/Roboto-Medium.ttf diff --git a/fonts/Roboto/Roboto-MediumItalic.ttf b/fonts/Roboto/Roboto-MediumItalic.ttf Binary files differnew file mode 100644 index 0000000..dc743f0 --- /dev/null +++ b/fonts/Roboto/Roboto-MediumItalic.ttf diff --git a/fonts/Roboto/Roboto-Regular.ttf b/fonts/Roboto/Roboto-Regular.ttf Binary files differnew file mode 100644 index 0000000..8c082c8 --- /dev/null +++ b/fonts/Roboto/Roboto-Regular.ttf diff --git a/fonts/Roboto/Roboto-Thin.ttf b/fonts/Roboto/Roboto-Thin.ttf Binary files differnew file mode 100644 index 0000000..d695550 --- /dev/null +++ b/fonts/Roboto/Roboto-Thin.ttf diff --git a/fonts/Roboto/Roboto-ThinItalic.ttf b/fonts/Roboto/Roboto-ThinItalic.ttf Binary files differnew file mode 100644 index 0000000..07172ff --- /dev/null +++ b/fonts/Roboto/Roboto-ThinItalic.ttf 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 + |
