Módulo:Mercado

De RuneScape Wiki
Ir para: navegação, pesquisa

A documentação para este módulo pode ser criada em Módulo:Mercado/doc

--[[
{{Helper module|name=Exchange
|fname1=_price(arg)
|ftype1=String
|fuse1=Gets the current median price of item named arg
|fname2=_value(arg)
|ftype2=String
|fuse2=Gets the value of item named arg
}}
--]]
-- <nowiki>
--
-- Implements various exchange templates
-- See Individual method docs for more details
--
-- See also:
-- - [[Módulo:ExchangeData]]
-- - [[Módulo:ExchangeDefault]]
--

local p = {}

-- only load commonly used modules here
local yesno = require( 'Módulo:Yesno' )
local addcommas = require( 'Módulo:Addcommas' )._add

-- map redirects to their correct pages
local geRedirects = {
    ['1/2 anchovy pizza'] = '½ anchovy pizza',
    ['1/2 meat pizza'] = '½ meat pizza',
    ["1/2 p'apple pizza"] = "½ p'apple pizza",
    ['1/2 plain pizza'] = '½ plain pizza',
    ['1/3 evil turnip'] = '⅓ evil turnip',
    ['1/3 blue blubber jellyfish'] = '⅓ blue blubber jellyfish',
    ['1/3 green blubber jellyfish'] = '⅓ green blubber jellyfish',
    ['2/3 cake'] = '⅔ cake',
    ['2/3 chocolate cake'] = '⅔ chocolate cake',
    ['2/3 evil turnip'] = '⅔ evil turnip',
    ['2/3 blue blubber jellyfish'] = '⅔ blue blubber jellyfish',
    ['2/3 green blubber jellyfish'] = '⅔ green blubber jellyfish'
}

--
-- Makes sure first letter of item is uppercase
-- Automatically handles any redirects
--
local function checkTitle( item )
    -- upper case first letter to make sure we can find a valid item page
    item = mw.ustring.gsub( item, '&#0?39;', "'" )
    item = mw.ustring.gsub( item, '_', ' ' )
    item = mw.ustring.gsub( item, '  +', ' ' )
    item = mw.text.split( item, '' )
    item[1] = mw.ustring.upper( item[1] )
    item = table.concat( item )

    -- automatically handle redirects
    if geRedirects[item] ~= nil then
        item = geRedirects[item]
    end

    return item
end
--
-- Simple mw.loadData wrapper used to access data located on module subpages
--
-- @param item {string} Item to retrieve data for
-- @return {table} Table of item data
--
local function load( item )
    item = checkTitle( item )
    local noErr, ret = pcall( mw.loadData, 'Módulo:Mercado/' .. item )

    if noErr then
        return ret
    end

    error( ret )
end

local all_prices_module = nil
local function loadPrice( item )
    item = checkTitle( item )
	if all_prices_module == nil then
		all_prices_module = {} --mw.loadData('Módulo:GEPrices/data')
	end
	if all_prices_module[item] then
		return all_prices_module[item]
	else
		-- fallback to older method
		-- should only trigger when: invalid item called, or new (or newly moved) item called
		local noErr, ret = pcall(load, item)
		if noErr then
			return ret.price
		end
	end
	error('Price not found for '..item)
end

--
-- Returns the price of an item
--
-- @param item {string} Item to get current price of
-- @param multi {number} (optional) Multiplies the output price by the specified number
-- @param format {boolean} (optional) Format the result with commas (defaults to false)
-- @param round {number} (optional) Round the result to a number of decimal places
-- @return {number|string} Price of item. Will return a string if formatted, else a number.
--
function p._price( item, multi, format, round )
    local price = loadPrice( item )
    local multi = type( multi ) == 'number' and multi or 1
    local format = type( format ) == 'boolean' and format or false
    local ret = price * multi
    
    -- round the number to X d.p.
    if round ~= nil then
        local multi = 10^( round )
        ret = math.floor( ret * multi + 0.5 ) / multi
    end

    if format then
        return addcommas( ret )
    end

    return ret
end
function p._priceViaModule( item, multi, format, round )
    local price = load( item ).price
    local multi = type( multi ) == 'number' and multi or 1
    local format = type( format ) == 'boolean' and format or false
    local ret = price * multi
    
    -- round the number to X d.p.
    if round ~= nil then
        local multi = 10^( round )
        ret = math.floor( ret * multi + 0.5 ) / multi
    end

    if format then
        return addcommas( ret )
    end

    return ret
end

--
-- Returns the historical price of an item on (or near) a given date
--
-- @param iDate {string} Desired date (ex. "28 July 2020") of historical price
-- @param item {string} Item to get historical price of
-- @param multi {number} (optional) Multiplies the output price by the specified number
-- @param format {boolean} (optional) Format the result with commas (defaults to false)
-- @param round {number} (optional) Round the result to a number of decimal places
-- @return {number|string} Price of item. Will return a string if formatted, else a number.
--
function p._priceHist( iDate, item, multi, format, round )
    item = checkTitle( item )
    local multi = type( multi ) == 'number' and multi or 1
    local format = type( format ) == 'boolean' and format or false
    
    -- convert date to Unix timestamp
    local lang = mw.getContentLanguage()
    local noErr, data = pcall( lang.formatDate, lang, 'U', iDate )
    if not noErr then
    	error( 'Invalid date: ' .. iDate )
	else
		iDate = tonumber( data )
	end
    
    -- load historical data for item
    local noErr, data = pcall( mw.loadData, 'Módulo:Mercado/' .. item .. '/Data' )
    if not noErr then
        error( 'Historical price data not found for ' .. item )
    end

	-- iterate through the data searching for the given date (or the closest match)
	-- mw.loadData's metatable doesn't provide a way to get the number of entries,
	-- so can't do something cute like a binary search here... hope this isn't too expensive
    local histDate, histPrice
    local histDiff = 1e10
    for k, v in ipairs( data ) do
    	local tempDate, tempPrice = v:match( '(%d+):(%d+)' )
    	tempDate = tonumber( tempDate )
    	local tempDiff = math.abs( iDate - tempDate )
    	
    	if tempDate == iDate then
    		-- exact match
    		histDate = tempDate
    		histPrice = tempPrice
    		break
		elseif tempDiff > histDiff then
			-- difference is increasing; previous entry is best
			break
		else
			-- difference is same or decreasing; store current data but keep searching for better match
			histDiff = tempDiff
			histDate = tempDate
			histPrice = tempPrice
		end
	end
	
	if histPrice == nil then
		error( 'Failed to parse historice price data for ' .. item )
	end
	
	histPrice = histPrice * multi
    
    -- round the number to X d.p.
    if round ~= nil then
        local decShift = 10^( round )
        histPrice = math.floor( histPrice * decShift + 0.5 ) / decShift
    end

    if format then
    	histPrice = addcommas( histPrice )
    	local prefix = histDate ~= iDate and '~' or ''
    	
    	return '<span title="Historical price on ' .. lang:formatDate( 'Y-m-d', '@' .. tostring( histDate ) ) .. '" '
    		.. 'style="cursor: help; border-bottom: 1px dotted">' .. prefix .. histPrice .. '</span>'
    end

    return histPrice
end

--
-- Returns the limit of an item
--
-- @param item {string} Item to get the limit of
-- @return {number} Limit of item
--
function p._limit( item )
    return load( item ).limit
end

--
-- Returns the value of an item
--
-- @param item {string} Item to get the value for
-- @return {number} Value of item
--
function p._value( item )
    return load( item ).value
end

--
-- Returns the alchability of an item
--
-- @param item {string} Item to get the alchability of
-- @return {boolean} Alchability
--
function p._alchable( item )
	local a = load( item ).alchable
	if a == nil or a == true then
		return true
	elseif a == false then
		return false
	end
	return nil
end

--
-- Returns the alch multiplier of an item
--
-- @param item {string} Item to get the multiplier or
-- @return {number} Multiplier
--
function p._alchmultiplier( item )
	local a = load( item ).alchmultiplier
	if type(a) == 'number' then
		return a
	end
	return 0.6
end


--
-- Internal function for alch values
--
-- @param item {string} Item to get the high alch for
-- @param mul {number} Alchemy multiplier
-- @return {number} Alch value of item
--
function alchval(item, mul)
	if p._alchable(item) then
		local v = p._value(item)
		local m = p._alchmultiplier(item)
		if v then
			return math.max(math.floor(v * m * mul), 1)
		end
	end
	return  -1
end

--
-- Returns the high alch value of an item
--
-- @param item {string} Item to get the high alch for
-- @return {number} High alch of item
--
function p._highalch( item )
	return alchval(item, 1)
end

--
-- Returns the low alch value of an item
--
-- @param item {string} Item to get the low alch for
-- @return {number} Low alch of item
--
function p._lowalch( item )
	return alchval(item, 2/3)
end

--
-- Calculates the difference between the current price and the last price of an item
--
-- @param item {string} Item to calculate price difference for
-- @param format {boolean} `true` if the output is to be formatted with commas
--                         Defaults to `false`
-- @return {number|string} The price difference as a number
--                         If `format` is set to `true` then this returns a string
--                         If either of the prices to calculate the diff from are unavailable, this returns `0` (number)
--
function p._diff( item, format )
    local data = load( item )
    local diff = 0

    if data.price and data.last then
        diff = data.price - data.last

        if format then
            diff = addcommas( diff )
        end
    end

    return diff
end

--
-- {{GEItem}} internal method
--
-- @todo merge into p.table
--
-- @param item {string} Item to get data for
-- @return {string}
--
function p._table( item )
    -- load data and any required modules
    local item = checkTitle( item )
    local data = load( item )
    local timeago = require( 'Módulo:TimeAgo' )._ago
    local changeperday = require( 'Módulo:ChangePerDay' )._change

    -- set variables here to make the row building easier to follow
    local div = '<i>Sem informação</i>'
    local limit = data.limit and addcommas( data.limit ) or '<i>Sem informação</i>'
    local members = '<i>Sem informação</i>'

    if data.last then
        local link = 'http://services.runescape.com/l=3/m=itemdb_rs/viewitem.ws?obj=' .. data.itemId
        local change = math.abs( changeperday( {data.price, data.last, data.date, data.lastDate} ) )

        if data.price > data.last then
            arrow = '[[Arquivo:Up.svg|17px|link=' .. link .. ']]'
        elseif data.price < data.last then
            arrow = '[[Arquivo:Down.svg|17px|link=' .. link .. ']]'
        else
            arrow = '[[Arquivo:Unchanged.svg|17px|link=' .. link .. ']]'
        end

        if change >= 0.04 then
            arrow = arrow  .. arrow .. arrow
        elseif change >= 0.02 then
            arrow = arrow .. arrow
        end

        div = mw.html.create( 'div' )
            :css( 'white-space', 'nowrap' )
            :wikitext( arrow )

        div = tostring( div )
    end

    if data.members == true then
        members = '[[Arquivo:P2P ícone.png|30px|link=Membros]]'
    elseif data.members == false then
        members = '[[Arquivo:F2P ícone.png|30px|link=Jogadores gratuitos]]'
    end

    -- build table row
    local tr = mw.html.create( 'tr' )
        :tag( 'td' )
        	:addClass( 'inventory-image' )
            :wikitext( '[[Arquivo:' .. item .. '.png|' .. item .. ']]' )
            :done()
        :tag( 'td' )
            :css( {
                ['width'] = '15%',
                ['text-align'] = 'left'
            } )
            :wikitext( '[[' .. item .. ']]' )
            :done()
        :tag( 'td' )
            :wikitext( addcommas( data.price ) )
            :done()
        :tag( 'td' )
            :wikitext( div )
            :done()

    if data.alchable == nil or yesno( data.alchable ) then
        local low, high = '<i>Sem informação</i>', '<i>Sem informação</i>'

        if data.value then
            low = addcommas( p._lowalch(item) )
            high = addcommas( p._highalch(item) )
        end

        tr
            :tag( 'td' )
                :wikitext( low )
                :done()
            :tag( 'td' )
                :wikitext( high )
                :done()
    else
        tr
            :tag( 'td' )
                :attr( 'colspan', '2' )
                :wikitext( '<i>Cannot be alchemised</i>' )
                :done()
    end

    tr
        :tag( 'td' )
            :wikitext( limit )
            :done()
        :tag( 'td' )
            :wikitext( members )
            :done()
        :tag( 'td' )
            :css( 'white-space', 'nowrap' )
            :wikitext( '[[Mercado:' .. item .. '|ver]]' )
            :done()
        :tag( 'td' )
            :css( 'font-size', '85%' )
            :wikitext( timeago{data.date} )
            :done()

    return tostring( tr )

end

--
-- {{GEExists}}
--
function p.exists( frame )
    local args = frame:getParent().args
    local item = checkTitle( args[1] or '' )
    local noErr, data = pcall( mw.loadData, 'Módulo:Mercado/' .. item )

    if noErr then
        return '1'
    end

    return '0'
end

--
-- GEExists for modules
--
function p._exists( arg )
    local item = checkTitle( arg or '' )
    local noErr, data = pcall( mw.loadData, 'Módulo:Mercado/' .. item )

    if noErr then
        return true
    end

    return false
end

--
-- Internal method for p.highAlchTable, p.lowAlchTable and p.genStoreTable
--
-- @param item {string} The name of the item
-- @param data {table} The item's ge data
-- @param alch {number} The item's alch/sell value
-- @param min {number} (optional) Sets the cap for amount of items that can be converted to gp per hour
-- @param natPrice {number} (optional) Sets the price of a Nature rune (set to `0` by `p.genStoreTable`)
-- @param multi {number} (optional) Multiplies the profit by the specified number to allow calculating the profit for intervals other than 4 hours
--
local function alchTable( item, data, alch, min, natPrice, multi )
    local timeago = require( 'Módulo:TimeAgo' )._ago
    local round = require( 'Módulo:Number' )._round
    -- gen store doesn't need a nat price as it's not used
    -- therefore we'd set it to 0
    local natPrice = natPrice or load( 'Runa da naturaleza' ).price
    local multi = multi or 1
    local profit = alch - data.price - natPrice

    local image = '[[Arquivo:' .. item .. '.png|' .. item .. ']]'
    local itemStr = '[[' .. item .. ']]'
    local priceStr = addcommas( data.price )
    local alchStr = addcommas( alch )
    local profitStr = addcommas( profit )
    local roi = tostring( round( ( profit / ( data.price + natPrice ) * 100 ), 1 ) ) .. '%'
    local limit = data.limit and addcommas( data.limit ) or '<i>Sem informação</i>'
    local maxProfit = '<i>Sem informação</i>'
    local members = '<i>Sem informação</i>'
    local members_sortkey = 2
    local details = '[[Mercado:' .. item .. '|ver]]'
    local lastUpdated = timeago{data.date}

    if data.limit then
        -- cap at 4800, the maximum number of alchs that can be cast every 4 hours
        -- varies for general store rows
        min = min or 4800 
        min = ( data.limit > min ) and min or data.limit
        maxProfit = addcommas( min * profit * multi)
    end

    mw.log( maxProfit )

    if data.members == true then
        members = '[[Arquivo:P2P ícone.png|30px|link=Membros]]'
        members_sortkey = 1
    elseif data.members == false then
        members = '[[Arquivo:F2P ícone.png|30px|link=Jogadores gratuitos]]'
        members_sortkey = 0
    end

    local tr = mw.html.create( 'tr' )
        :tag( 'td' )
            :wikitext( image )
            :done()
        :tag( 'td' )
            :css( {
                width = '15%',
                ['text-align'] = 'left'
            } )
            :wikitext( itemStr )
            :done()
        :tag( 'td' )
            :wikitext( priceStr )
            :done()
        :tag( 'td' )
            :wikitext( alchStr )
            :done()
        :tag( 'td' )
            :wikitext( profitStr )
            :done()
        :tag( 'td' )
            :wikitext( roi )
            :done()
        :tag( 'td' )
            :wikitext( limit )
            :done()
        :tag( 'td' )
            :wikitext( maxProfit )
            :done()
        :tag( 'td' )
            :wikitext( members )
            :attr('data-sort-value', members_sortkey)
            :done()
        :tag( 'td' )
            :css( 'white-space', 'nowrap' )
            :wikitext( details )
            :done()
        :tag( 'td' )
            :css( 'font-size', '85%' )
            :wikitext( lastUpdated )
            :done()

    return tostring( tr )
end

--
-- {{HighAlchTableRow}}
--
-- @example {{HighAlchTableRow|<item>}}
--
function p.highAlchTable( frame )
    local args = frame:getParent().args
    local item = checkTitle( args[1] )
    local data = load( item )
    local alch = p._highalch(item)

    return alchTable( item, data, alch )
end
--
-- {{LowAlchTableRow}}
--
-- @example {{LowAlchTableRow|<item>}}
--
function p.lowAlchTable( frame )
    local args = frame:getParent().args
    local item = checkTitle( args[1] )
    local data = load( item )
    local alch = p._lowalch(item)

    return alchTable( item, data, alch )
end

--
-- {{GenStoreTableRow}}
--
-- @example {{GenStoreTableRow|<item>}}
--
function p.genStoreTable( frame )
    local args = frame:getParent().args
    local item = checkTitle( args[1] )
    local data = load( item )
    local alch = math.floor( data.value * 0.3 )

    return alchTable( item, data, alch, 50000, 0 )
end

--
-- {{Alchemiser2TableRow}}
--
-- @example {{Alchemiser2TableRow|<item>}}
--
function p.alchemiser2Table( frame )
    local args = frame:getParent().args
    local item = checkTitle( args[1] )
    local data = load( item )
    local alch = p._highalch(item)
    local round = require( 'Módulo:Number' )._round
    local divPrice = load( 'Carga divina' ).price
    -- calculate the price of nature rune and divine charges for 1 item
    local natPrice = load( 'Runa da naturaleza' ).price + round( ( divPrice / 500 ), 1)

    return alchTable( item, data, alch, 100, natPrice, 6 )
end

--
-- {{GEP}}
-- {{GEPrice}}
--
-- @example {{GEPrice|<item>|<format>|<multi>}}
-- @example {{GEPrice|<item>|<multi>}}
-- @example {{GEP|<item>|<multi>}}
--
function p.price( frame )
    -- usage: {{foo|item|format|multi}} or {{foo|item|multi}}
    local args = frame.args
    local pargs = frame:getParent().args
    local item = pargs[1]
    local expr = mw.ext.ParserFunctions.expr
    local round = tonumber( pargs.round )

    if item then
        item = mw.text.trim( item )
    else
        error( '"item" argument not specified', 0 )
    end

    -- default to formatted for backwards compatibility with old GE templates
    local format = true
    local multi = 1

    -- format is set with #invoke
    -- so set it first to allow it to be overridden by template args
    if args.format ~= nil then
        format = yesno( args.format )
    end

    if tonumber( pargs[2] ) ~= nil then
        multi = tonumber( pargs[2] )

    -- indicated someone is trying to pass an equation as a mulitplier
    -- known use cases are fractions, but pass it to #expr to make sure it's handled correctly
    elseif pargs[2] ~= nil and mw.ustring.find( pargs[2], '[/*+-]' ) then
        multi = tonumber( expr( pargs[2] ) )

    -- uses elseif to prevent something like {{GEP|Foo|1}}
    -- causing a formatted output, as 1 casts to true when passed to yesno
    elseif type( yesno( pargs[2] ) ) == 'boolean' then
        format = yesno( pargs[2] )

        if tonumber( pargs[3] ) ~= nil then
            multi = tonumber( pargs[3] )
        end
    end 

    return p._price( item, multi, format, round )
end

--
-- {{GEPHist}}
-- {{GEPriceHist}}
--
-- @example {{GEPriceHist|<item>|<format>|<multi>|date=<date>}}
-- @example {{GEPriceHist|<item>|<multi>|date=<date>}}
-- @example {{GEPHist|<item>|<multi>|date=<date>}}
--
function p.priceHist( frame )
    -- usage: {{foo|item|format|multi|date=<date>}} or {{foo|item|multi|date=<date>}}
    local args = frame.args
    local pargs = frame:getParent().args
    local iDate = pargs.date
    local item = pargs[1]
    local expr = mw.ext.ParserFunctions.expr
    local round = tonumber( pargs.round )

    if iDate then
        iDate = mw.text.trim( iDate )
    else
        error( '"date" argument not specified', 0 )
    end
    
    if item then
        item = mw.text.trim( item )
    else
        error( '"item" argument not specified', 0 )
    end

    -- default to formatted for backwards compatibility with old GE templates
    local format = true
    local multi = 1

    -- format is set with #invoke
    -- so set it first to allow it to be overridden by template args
    if args.format ~= nil then
        format = yesno( args.format )
    end

    if tonumber( pargs[2] ) ~= nil then
        multi = tonumber( pargs[2] )

    -- indicated someone is trying to pass an equation as a mulitplier
    -- known use cases are fractions, but pass it to #expr to make sure it's handled correctly
    elseif pargs[2] ~= nil and mw.ustring.find( pargs[2], '[/*+-]' ) then
        multi = tonumber( expr( pargs[2] ) )

    -- uses elseif to prevent something like {{GEP|Foo|1}}
    -- causing a formatted output, as 1 casts to true when passed to yesno
    elseif type( yesno( pargs[2] ) ) == 'boolean' then
        format = yesno( pargs[2] )

        if tonumber( pargs[3] ) ~= nil then
            multi = tonumber( pargs[3] )
        end
    end 

    return p._priceHist( iDate, item, multi, format, round )
end

--
-- {{GEItem}}
--
-- @example {{GEItem|<item>}}
--
function p.table( frame )
    local args = frame:getParent().args
    local item = args[1]

    if item then
        item = mw.text.trim( item )
    else
        error( '"item" argument not specified', 0 )
    end

    return p._table( item )
end

--
-- experimental limit method for [[Grand Exchange/Buying Limits]]
--
function p.gemwlimit( frame )
    local item  = frame:getParent().args[1]
    local data = mw.loadData( 'Módulo:Mercado/' .. item )
    
    return data.limit
end

--
-- {{ExchangeItem}}
-- {{GEDiff}}
-- {{GELimit}}
-- {{GEValue}}
-- {{GEId}}
--
-- @example {{ExchangeItem|<item>}}
-- @example {{GEDiff|<item>}}
-- @example {{GELimit|<item>}}
-- @example {{GEValue|<item>}}
-- @example {{GEId|<item>}}
--
function p.view( frame )
    local fargs = frame.args
    local pargs = frame:getParent().args
    local item = pargs[1] or fargs.item
    local view = fargs.view or ''
    local loadView = {limit=true, value=true, itemId=true, members=true, category=true, examine=true, alchable=true}

    if item then
        item = mw.text.trim( item )
    else
        error( '"item" argument not specified', 0 )
    end

    view = mw.ustring.lower( view )

    if view == 'itemid' then
        view = 'itemId'
    end

    if view == 'diff' then
        return p._diff( item )

    elseif loadView[view] then
        return load( item )[view]

    else
        local default = require( 'Módulo:ExchangeDefault' )
        -- handle redirects and casing of item before passing it on
        item = checkTitle( item )
        return default.main( item )
    end
end

return p
-- </nowiki>