diff --git a/README.md b/README.md index 66bb82c..26e043c 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,6 @@ human readability. This is done by looking for patterns in the input data, and creating an output string utilizing and highlighting those patterns. Thus it's a primarily a debugging tool, not a speedy serialization tool. -Humans are flexible in their understanding of data, as long as certain -underlying structural patterns can be found. `pretty` attempts to find those -patterns and highlight them, often after performing huge analytical tasks. - Sometimes just aligning elements are enough to easy the burden on the eyes. Contrast the two following pieces of code: @@ -33,8 +29,22 @@ printer is much simpler than `pretty`, but the enhancements compared to other pretty printers, inspired me to create `pretty`. See the bottom of the page for other pretty printers. +`pretty` sorts it's priorities like so: + +1. Human readability. +2. Lua-compatible output. +3. Customization support. + +I'd rather have good defaults than provide a ton of customization options. And +if some structure cannot be represented in Lua, I will rather extend the +syntax, than lose the info. + Another aspect where `pretty` shines is in exploratory programming, when -attempting to avoid reliance on outside documentation. +attempting to avoid reliance on outside documentation. The amount of information +`pretty` exposes varies by the data you are inspecting. If you're inspecting +a list of functions, their function signatures are visible, but if you're +inspecting a single function, documentation and source location may appear if +available. ## Features @@ -46,7 +56,7 @@ attempting to avoid reliance on outside documentation. * Keys-value pairs are [properly](http://www.davekoelle.com/alphanum.html) sorted by key type and thereafter alphabetically. * The format and structure of output changes depending upon the input. - Maps are displayed differently to deeply nested tables to long sequences + Maps appear differently to deeply nested tables to long sequences with short strings to short lists. * Uses the standard `debug` library to gain information about functions and other advanced structures. @@ -72,15 +82,16 @@ I'm looking into implementing following features: - Nice formatting for `cdata` datatype in LuaJIT. - Add possibility of comments in output, for stuff like `__tostring` methods, and global namespaces like `io` or `math`. -- Better support upvalues in functions. Complete support is impossible without - traversing the original code or inspecting the intermediate representation, - due to lexical scoping. (Pluto does it, but it's written in C.) +- Better support upvalues for in functions. Complete support is impossible + without traversing the original code or inspecting the intermediate + representation, due to lexical scoping. (Pluto does it, but it's written in + C.) - Look more into `string.dump` in the core library. - Look into using concat operation to improve appearance of overly long non-breaking strings. Maybe even attempt to break near whitespace. - Attempt to fit output within a predefined width limit. Default to 80(?) - Find a better name than `pretty`. -- Add options for colored output, maybe even support multiple? +- Add options for colored output, and allow custom formatting. ## Other pretty printers diff --git a/library.lua b/library.lua index ba93082..c2f1ac6 100644 --- a/library.lua +++ b/library.lua @@ -1,4 +1,6 @@ +-- TODO: Make lazy? It imports a lot of packages tbh. + local function_library = {} local function library_function ( func_def ) diff --git a/number.lua b/number.lua index fd6e5fe..3b43ec0 100644 --- a/number.lua +++ b/number.lua @@ -1,10 +1,4 @@ -local utf8 = require "utf8" -- FIXME: I don't really like this. Either have a local copy of the files, or remove the functionallity requiring it. - --- Make sure we imported the correct library. -local CAN_USE_SHORTHAND = not not utf8.width -if not CAN_USE_SHORTHAND then utf8.width = function(a) return #a end end - -- Constants local MAXIMUM_INT = 2^53 -- The maximum double for where all integers can be represented exactly. @@ -13,35 +7,6 @@ local MAXIMUM_ZERO = 10^-7 -- Used when attempting to determine fraction. Anyt -------------------------------------------------------------------------------- -- Util -local function vulgar_fraction (a, b) - -- Returns a string representing the fraction a/b, with unicode characters. - return utf8.superscript(a) .. '⁄' .. utf8.subscript(b) -end - -local function is_integer (n) - -- Predicate. Neither +inf, -inf nor nan are integer. - return n == math.floor(n) and n ~= 1/0 and n ~= -1/0 -end - -local function factorial (n) - -- Returns the factorial of n. - -- That is n! = n * (n-1) * (n-2) * ... * 1 - local a = 1 - for i = 2, n do a = a * i end - return a -end - -local function inverse_factorial (n) - -- Returns the inverse factorial of n. - -- That is some m, where n = m! - local a, i = 1, 1 - while a < n do - i = i + 1 - a = a * i - end - return (a == n) and i or nil -end - local function calculate_fraction (n) -- Returns x and y such that x/y = n. If none could be found, returns nil. local a, b = 1, n % 1 @@ -59,89 +24,68 @@ end -------------------------------------------------------------------------------- -local format_num - local SPECIAL_NUMBER = { -- x = ∞ - { est = function (...) return ... end, - real = function (a) return math.huge end, - repr = function (a) return '1/0' end, - short = function (a) return '∞' end, + { est = function (a) return math.huge end, + real = function (a) return math.huge end, + repr = function (a) return '1/0' end, }, - -- a = x - { est = function (n) return n end, - real = function (a) return a end, - repr = function (a) return ('%i'):format(a) end, - short = function (a) return ('%i'):format(a) end, - }, -- x = a/b { est = calculate_fraction, real = function (a, b) return b ~= 1 and (a/b) end, repr = function (a, b) return a..'/'..b end, - short = function (a, b) return a..'/'..b end, - }, - -- Factorial - { est = function (n) return inverse_factorial(n) end, - real = function (a) return factorial(a) end, - repr = function (a) return ('fac(%i)'):format(a) end, - short = function (a) return a..'!' end, }, -- x = 2^a { est = function (n) return math.log(n)/math.log(2) end, real = function (a) return 2^a end, repr = function (a) return '2^'..a end, - short = function (a) return '2'..utf8.superscript(a) end, }, -- x = 10^a { est = function (n) return math.log(n)/math.log(10) end, real = function (a) return 10^a end, repr = function (a) return '10^'..a end, - short = function (a) return '10'..utf8.superscript(a) end, }, -- x = 1/√a { est = function (n) return 1/(n^2) end, - real = function (a) return 1/math.sqrt(a) end, - repr = function (a) return ('1/math.sqrt(%i)'):format(a) end, - short = function (a) return '1/√'..utf8.overline(a) end, + real = function (a) return a >= 0 and 1/math.sqrt(a) end, + repr = function (a) return ('1/math.sqrt(%.0f)'):format(a) end, }, -- x = lg a { est = function (n) return math.exp(n) end, - real = function (a) return math.log(a) end, - repr = function (a) return ('math.log(%i)'):format(a) end, - short = function (a) return ('lg(%i)'):format(a) end, + real = function (a) return a >= 0 and math.log(a) end, + repr = function (a) return ('math.log(%.0f)'):format(a) end, }, -- x = ℯ^a { est = function (n) return math.log(n) end, real = function (a) return math.exp(a) end, - repr = function (a) return ('math.exp(%i)'):format(a) end, - short = function (a) return a == 1 and 'ℯ' or 'ℯ'..utf8.superscript(a)end, + repr = function (a) return ('math.exp(%.0f)'):format(a) end, }, -- x = aπ { est = function (n) return n/math.pi end, real = function (a) return a*math.pi end, repr = function (a) return a == 1 and 'math.pi' or a..'*math.pi' end, - short = function (a) return (a == 1 and '' or a)..'π' end, }, -- x = sqrt(a) { est = function (n) return n^2 end, - real = function (a) return math.sqrt(a) end, - repr = function (a) return ('math.sqrt(%i)'):format(a) end, - short = function (a) return '√'..utf8.overline(a) end, + real = function (a) return a >= 0 and math.sqrt(a) end, + repr = function (a) return ('math.sqrt(%.0f)'):format(a) end, }, } -------------------------------------------------------------------------------- -function format_num (n, shorthand, soft_numbers) - if n ~= n then return shorthand and 'NaN' or '0/0' - elseif n < 0 then return '-' .. format_num(-n, shorthand) +local function format_soft_num (n) + assert(type(n) == 'number') + + if n ~= n then return '0/0' + elseif n == 0 then return '0' + elseif n < 0 then return '-' .. format_soft_num(-n) end -- Finding the shortest local shortest, length = nil, math.huge local function alternative_repr (repr) - local repr_len = utf8.width(repr) - if repr_len < length then shortest, length = repr, repr_len end + if #repr < length then shortest, length = repr, #repr end end -- Maybe it's a "special" number? @@ -151,10 +95,10 @@ function format_num (n, shorthand, soft_numbers) for i = 1, #a do a[i] = math.floor(a[i] + 0.5) end local num = special_number_tests.real(unpack(a)) if num == n then - alternative_repr( special_number_tests[shorthand and 'short' or 'repr'](unpack(a)) ) - elseif num and soft_numbers then - local repr = special_number_tests[shorthand and 'short' or 'repr'](unpack(a)) - local native_precise = tonumber(('%'..utf8.width(repr)..'f'):format(n)) + alternative_repr( special_number_tests.repr(unpack(a)) ) + elseif num then + local repr = special_number_tests.repr(unpack(a)) + local native_precise = tonumber(('%'..#repr..'f'):format(n)) if math.abs(num - n) <= math.abs( native_precise - n ) then alternative_repr( repr ) end @@ -163,14 +107,54 @@ function format_num (n, shorthand, soft_numbers) end -- Maybe it's a decimal number? - alternative_repr( tostring(n):gsub('([^e]+)e%+?(%-?)0+', function(a, b) return (a == '1' and '' or a..'*')..'10^'..b end)) + alternative_repr( tostring(n):gsub('([^e]+)e%+?(%-?[^e]*)', function(a, b) return (a == '1' and '' or a..'*')..'10^'..b end)) -- Well, this is not a pretty number! return shortest end +local function format_hard_num (n) + assert(type(n) == 'number') + + -- All the fun special cases + if n ~= n then return '0/0' + elseif n == 0 then return '0' + elseif n == math.huge then return '1/0' + elseif n == -math.huge then return '-1/0' + end + + -- Now for the serious part. + for i = 0, 99 do + local repr = ('%.'..i..'f'):format(n) + local num = tonumber(repr) + if num == n then return repr end + end + + assert(false) +end + return function (value, depth, l) + -- Formats the number nicely. If depth is 0 and we have some space for extra + -- info, we give some tidbits, to help investigation. + assert(type(value) == 'number') assert(type(depth) == 'number' and type(l) == 'table') - l[#l+1] = format_num(value, CAN_USE_SHORTHAND and l.options.math_shorthand, l.options.soft_numbers) + + -- First format a "soft" version. This number is not guarenteed to accurate. + -- It's purpose is to give a general idea of the value. + l[#l+1] = format_soft_num(value) + + -- If we have space for it, format a "hard" value, also. This number is as + -- short as possible, while evaluating precisely to the value of the number. + if depth == 0 then + local hard_repr = format_hard_num(value) + if l[#l] ~= hard_repr then + l[#l+1] = ' -- Approx: ' + l[#l+1] = hard_repr + end + -- TODO: Add extra information. I don't really know what is useful? + -- Prime factorization is fun, but useless unless people are doing + -- cryptography or general number theory. + -- Bit pattern is maybe too lowlevel. + end end diff --git a/pretty.lua b/pretty.lua index 48c0f77..f935e4c 100644 --- a/pretty.lua +++ b/pretty.lua @@ -471,8 +471,6 @@ local KNOWN_OPTIONS = { cut_strings = { type = 'boolean', default = false }, include_closure = { type = 'boolean', default = false }, indent = { type = 'string', default = ' ' }, - math_shorthand = { type = 'boolean', default = false }, - soft_numbers = { type = 'boolean', default = true }, -- TODO: Add support for maximally precise numbers. max_depth = { type = 'number', default = math.huge }, embed_loaded_funcs = { type = 'boolean', default = false }, more_function_info = { type = 'boolean', default = false }, diff --git a/test/test_number.lua b/test/test_number.lua index 438daaf..17fe2f7 100644 --- a/test/test_number.lua +++ b/test/test_number.lua @@ -11,11 +11,13 @@ local function format_test (t) end local function number_test (t) - SUITE:addTest(t.name or t.expect, function () - assert_equal(t.expect, format(t.input, {})) - end) - SUITE:addTest(t.name and (t.name .. ' shorthand') or t.shorthand or (t.expect .. ' shorthand'), function () - assert_equal(t.shorthand or t.expect, format(t.input, { math_shorthand = true })) + if t.single then + SUITE:addTest((t.name or t.expect)..' single', function () + assert_equal(t.single, format(t.input, {})) + end, { line = debug.getinfo(2).currentline }) + end + SUITE:addTest((t.name or t.expect) .. ' small', function () + assert_equal('{ '..t.expect..' }', format({t.input}, {})) end, { line = debug.getinfo(2).currentline }) end @@ -35,6 +37,7 @@ number_test { number_test { input = 2000, expect = '2000', + single = '2000', } number_test { @@ -48,25 +51,21 @@ number_test { number_test { input = 10^5, expect = '10^5', - shorthand = '10⁵', } number_test { input = 10^-6, expect = '10^-6', - shorthand = '10⁻⁶', } number_test { input = 2^17, expect = '2^17', - shorthand = '2¹⁷', } number_test { input = 2^-7, expect = '2^-7', - shorthand = '2⁻⁷', } -------------------------------------------------------------------------------- @@ -92,6 +91,13 @@ number_test { expect = '2.0512523', } +number_test { + name = 'Large decimal number', + input = 60982348080952348324.42342, + expect = '6.0982348080952*10^19', + single = '6.0982348080952*10^19 -- Approx: 60982348080952344576', +} + -------------------------------------------------------------------------------- -- Fractions @@ -111,26 +117,25 @@ number_test { number_test { input = math.sqrt(8), expect = 'math.sqrt(8)', - shorthand = '√̅8', } number_test { input = 1/math.sqrt(8), expect = '1/math.sqrt(8)', - shorthand = '1/√̅8', } number_test { input = math.log(8), expect = 'math.log(8)', - shorthand = 'lg(8)', } +--[[ Factorials are fun, but they are not inheritly supported in the language. number_test { input = 6227020800, expect = 'fac(13)', - shorthand = '13!', + single = '6227020800', } +]] -------------------------------------------------------------------------------- -- Constants, and multiples of constants @@ -138,43 +143,36 @@ number_test { number_test { input = math.pi, expect = 'math.pi', - shorthand = 'π', } number_test { input = 7*math.pi, expect = '7*math.pi', - shorthand = '7π', } number_test { input = math.exp(1), expect = 'math.exp(1)', - shorthand = 'ℯ', } number_test { input = math.exp(10), expect = 'math.exp(10)', - shorthand = 'ℯ¹⁰', } number_test { input = 1/0, expect = '1/0', - shorthand = '∞' } number_test { input = -1/0, expect = '-1/0', - shorthand = '-∞' } number_test { input = 0/0, expect = '0/0', - shorthand = 'NaN', } -------------------------------------------------------------------------------- @@ -187,12 +185,6 @@ do name = 'Approx π', input = sum, expect = 'math.pi', - shorthand = 'π', - } - format_test { - input = sum, - options = { soft_numbers = false }, - expect = tostring(sum), } end @@ -208,6 +200,7 @@ number_test { -- A half should always be represented using 1/2, never with 2⁻¹. input = 1/2, expect = '1/2', + single = '1/2 -- Approx: 0.5' } format_test { diff --git a/test/tests.lua b/test/tests.lua index dad8e7b..d175220 100644 --- a/test/tests.lua +++ b/test/tests.lua @@ -3,5 +3,6 @@ package.path = package.path .. ';./test/?.lua;./src/?.lua' local TEST_SUITE = require("TestSuite").new('pretty') TEST_SUITE:addModules('test/test_*') +-- TEST_SUITE:generateRequireSubmodule 'require' TEST_SUITE:setOptions(...) TEST_SUITE:runTests()