-- pretty.number
-- The number formatting module for pretty.

--[=[ Thoughts on displaying numbers in an informative way.

Numbers are such an intrinsic part of computer science, mathematics, science in
general and even finance industry, that it boggles they mind how different they
each treat numbers. This is one of the reasons that many programming languages
possess a huge number of number types: Integers both signed and unsigned,
rational number, floating point numbers, fixed point numbers, not to mention the
various Computer Algebra Systems (CAS), that exist.

Lua has historically used floating point numbers, which can represent numbers in
a huge range, at the cost of non-uniform precision across that range. How can we
represent numbers in a intuitive and useful way?

1.	Native representation: We could use Lua's native way of representing
	numbers. This is easy, as we just use the `tostring` function.
	This is unsatifactory, if we're aiming for Lua-compatible output, as
	`tostring` will produce `inf` (`1/0`) and `nan` (`0/0`), which are valid
	identifiers, and does not guarentee that these variables are equal to their
	respective values.
	It's also unsatifactory when aiming for precision, as `tostring` truncate
	the output if "close enough".
2.	Unnecessary precision: We can use `string.format` with the format code
	`%.99f`, to get a "precise" representation of the number. Unfortunantly, for
	many "larger" (> 10⁻¹³?) numbers, some of the right digits will be 0, due
	to floating point precision issues. We also run against the fact that π is
	rounded and is only accurate 15 decimal digits. Do we really care about the
	remaining digits, most of which are 0?
3.	As required representation: We use as many digits as necessary for the Lua
	parser to read the exact same number. Involves invoking `string.format` and
	`tonumber` a lot.
	This approach improves on 2. as we not only discard all those 0s and the
	confusing unaccurate digits. Indeed, many "nice" numbers won't even possess
	any decimal digits. Numbers feed directly into the interpreter would look
	identical to the input, barring removal of trailing 0s.
	And with that we achieved precision and a reasonable amount of consiseness.
4.	Fractions: One issue with floating points have always been that the
	precision is sorely limited. Some languages possess fraction types, but
	such systems suffer from pathological cases, where memory usage spikes.
	We can still exploit fractions despite using floating point numbers. When we
	type `13/7` into Lua, we get a number back. This is the "canonical"
	floating point representation of `13/7`, and every time we encounter this
	number, we can use `13/7` to represent it.
	This allows more consise representation of certain numbers, for example
	numbers with repeating digits. It will also highlight patterns that a
	decimal representation might not.
	Unfortunantly it might also hide patterns, and it reduces the usefulness of
	pretty as a calculator.
5.	Reverse engineered number: Remember the concept of "canonical"
	representation from 4.? We can expand that concept to also talk about more
	general expressions like `math.sqrt(2)` or `2^-5`. Discovering those
	expressions from a single number is pretty complex.
	Again, this allows for very consise representations of numbers, and can find
	hidden patterns all over the place. It's much easier to see the relation to
	radians when we see `0.5*math.pi`, rather than `0.78539816339745`.
	But again we lose the calculator aspect.
6.	TODO: Write about `inf` and `nan`.
7.	TODO: Write about rounding.
8.	TODO: Write about unicode.

We a bit of every one of the above methods. They each have their upsides and
their drawbacks. Which approach we use, depend on the number, and what is the
shortest representation.
If the chosen representation is different from the "as required approach", and
we're only pretty printing a single number value, then we also append the
approximate value given by the "as required approach" approach as a comment, in
case somebody was looking for that.
--]=]

--------------------------------------------------------------------------------

local DISPLAY =  assert(require((... and select('1', ...):match('.+%.') or '')..'common'), '[pretty]: Could not load vital library: common') . DISPLAY

-- Constants

local MAXIMUM_INT  = 2^53    -- The maximum double for where all integers can be represented exactly.
local MAXIMUM_ZERO = 10^-7   -- Used when attempting to determine fraction. Anything below is counted as 0.

--------------------------------------------------------------------------------
-- Util

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
	while MAXIMUM_ZERO < b and a <= MAXIMUM_INT do
		local r = math.pow(b, -1)
		a, b = a * r, r % 1
		-- Invariant:  n * a / a = n
	end
	-- Check the values make sense.
	local numerator, denumberator = math.floor(n * a), math.floor(a)
	if numerator / denumberator == n then
		return numerator, denumberator
	end
end

--------------------------------------------------------------------------------

local SPECIAL_NUMBER = {
	-- x = ∞
	{ est   = function (a)   return math.huge                               end,
	  real  = function (a)   return math.huge                               end,
	  repr  = function (a)   return '1/0'                                   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,
    },
	-- 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,
    },
	-- 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,
    },
	-- x = 1/√a
	{ est   = function (n) return 1/(n^2)                                   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 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(%.0f)'):format(a)                end,
    },
	-- x = aπ
	-- TODO: Add support for factions of π.
	{ 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,
    },
	-- x = sqrt(a)
	{ est   = function (n) return n^2                                       end,
	  real  = function (a) return a >= 0 and math.sqrt(a)                   end,
	  repr  = function (a) return ('math.sqrt(%.0f)'):format(a)               end,
    },
}

--------------------------------------------------------------------------------

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)
		if #repr < length then  shortest, length  =  repr, #repr  end
	end

	-- Maybe it's a "special" number?
	for _, special_number_tests in pairs(SPECIAL_NUMBER) do
		local a = { special_number_tests.est(n) }
		if a[1] then
			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.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
			end
		end
	end

	-- Maybe it's a decimal number?
	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, display, l)
	-- Formats the number nicely. If display is DISPLAY.EXPAND, we have some
	-- space for extra info, we give some tidbits, to help investigation.

	assert(type(value) == 'number')
	assert(type(display) == 'number' and type(l) == 'table')

	-- 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 display == DISPLAY.EXPAND 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