模組:沙盒/TimWu007/RailSystems

文档图示 模块文档[查看] [编辑] [历史] [清除缓存]

调用 编辑

本模块支持调用下列函数来获取数据(或 MediaWiki 内容)。

编辑

调用名称 参数 说明
stationLink

{{#invoke:RailSystems | stationLink
| name=车站名
| system=铁道系统名
}}

通过车站名获取车站在铁道系统中的链接,用于处理条目名不规则或消歧义后的条目。

例如从“朝阳门”转换至“[[朝阳门站 (北京)|朝阳门]]”。

lineColor

{{#invoke:RailSystems | lineColor
| name=线路编号
| prefix=是否有“#”前缀
| system=铁道系统名
}}

通过线路编号获取线路颜色。若 prefix=1 则加上“#”前缀。
lineTitle

{{#invoke:RailSystems | lineTitle
| name=线路编号
| link=0/1
| system=铁道系统名
}}

通过线路编号获取线路条目链接。若 link=0 则只返回条目名,否则返回条目链接 Wiki Text,默认 link=1。
lineRichTitle

{{#invoke:RailSystems | lineRichTitle
| name=线路编号
| degrade=无标志色时,是否自动降级为 lineTitle 结果
| system=铁道系统名
}}

通过线路编号获取带有标志色装饰(默认为前置彩色小方块)的线路条目链接。若 degrade 为 1,则在标志色未定义时自动返回 lineTitle 的结果。
lineTerminal

{{#invoke:RailSystems | lineTerminal
| name=线路编号
| side=左侧(填left)或右侧(填right)
| type=终点站类型,可为空
| branch=终点站支线,可为空
| system=铁道系统名
}}

通过线路编号、方向、类型(可选)及支线(可选)获得线路终点站链接。
lineTerminalName

{{#invoke:RailSystems | lineTerminalName
| name=线路编号
| side=左侧(填left)或右侧(填right)
| type=终点站类型,可为空
| branch=终点站支线,可为空
| system=铁道系统名
}}

通过线路编号、方向、类型(可选)及支线(可选)获得线路终点站站名。
trainTime

{{#invoke:RailSystems | trainTime
| name=线路编号
| dir=运营方向
| type=首班(填first或F)或末班(填last或L)
| delta=起点站发车到本站到站的时间
| system=铁道系统名
}}

通过线路编号、运营方向(预定义)、首/末班及始发站到达本站的时间获得车站首班/末班车时间。
trainDirectionTime

{{#invoke:RailSystems | trainDirectionTime
| name=线路编号
| dir=运营方向
| type=首班(填first或F)或末班(填last或L)
| delta=起点站发车到本站到站的时间
| system=铁道系统名
}}

通过线路编号、运营方向(预定义)、首/末班及始发站到达本站的时间获得车站首班/末班车终点站名链接及时间组成的 WikiText。
lineDateMessage

{{#invoke:RailSystems | lineDateMessage
| 日期/时间格式串,可用多个管道符号分隔,
| name=线路编号
| type=线路期数,可为空
| auto_hide=0(超过给定日期不隐藏)/1(超过给定日期自动隐藏),默认为1
| auto_defer=0(比较时按给定日期比较)/1(比较时按给定日期延后一个时间单位比较),默认为0
}}

通过线路编号和期数,显示距离开通需要的时间。本函数支持自定义日期格式,year、month、day分别表示年、月、日(含单位),ym、date分别表示年月组合、日期,可通过管道符号“|”和其他自定义字符相连。如果 auto_hide 未指定或值为“1”,当该日期已过时,文字将自动隐藏,条目会被加入Category:需要去除时间判断模板的页面

使用建议 编辑

不建议直接在条目空间中使用 #invoke 语法调用本模块,而是为每个系统建立自己的系列模板,在模板中引用本模块。例如,北京地铁系列条目使用 {{BJS color}} 调用本模块的 lineColor 方法。

一部分其他模板也集成了本模板,包括参数 lua=1 条件下的 {{铁道路线}} 模板(参见 Module:RouteSequence)以及 {{S-line}} 中的一部分功能。{{RenderStations}} 利用了模块中的 renderStationLinks 方法,可以方便地对一段 WikiText 中的站名进行转换。

子模块列表 编辑

下面为已有的子模块列表,支持不同的铁道系统。注意,以“/doc”为后缀的是文档,而“UseCase”是测试用例


建立系统模板 编辑

将本模块引入铁道系统前,你需要首先创建一个系统数据模块。该模块为本模块的子模块,名称为 Module:RailSystems/<系统名> ,基本框架为

local p = {}

p.lines = {
	-- 线路列表
}

-- 开通日期,用于为未开通线路标记日期,可有多行,非必需
p.lines['线路名'].openDates = { ['#default'] = '默认开通日期', ['2'] = '二期开通日期' }
p.lines['线路名2'].openDates = '单一开通日期'

local lineAliases = {
	-- 线路别名(用于简繁转换)
}
-- 用于处理别名的代码,勿删
for k, v in pairs(lineAliases) do p.lines[k] = p.lines[v] end
-- 起讫站信息必需代码,勿删
for k, v in pairs(p.lines) do v.terminals = {} end
-- 首末班车信息必需代码,勿删
for k, v in pairs(p.lines) do v.trainTime = {} end

p.stationNames = {
	-- 站名链接
}

local stationAliases = {
	-- 车站别名
}
-- 用于处理别名的代码,勿删
for k, v in pairs(stationAliases) do p.stationNames[k] = p.stationNames[v] end

-- 起讫站
p.lines['1'].terminals = { left = 'XXX', right = 'XXX' }
p.lines['2'].terminals = {
	left = { ['#field'] = 'type', ['#default'] = 'XXX', F = 'XXX'},
	right = { 'XXX' },
}

-- 首末班车
p.lines['1'].trainTime = {
	['SE'] = { first = '06:00', last = '23:00', startService = 'XXX', endService = 'XXX' },
	['ES'] = { first = '06:00', last = '23:00', startService = 'XXX', endService = 'XXX' },
}

return p

线路列表 编辑

本模块线路列表的例子如下

p.lines = {
	['1'] = { title = "宁波轨道交通1号线|1号线", color = '3180b7' },
	['2'] = { title = "宁波轨道交通2号线|2号线", color = 'cc0000' },
}

其中,title 表示线路的条目名称及显示名称,使用“|”分隔。color 表示线路的标志色。

线路开通日期 编辑

本模块线路开通日期的例子如下。如果系统中无此需求可省略这部分。

p.lines['1'].openDates = { ['#default'] = '2014-05-30', ['2'] = '2016-03-19' }
p.lines['2'].openDates = '2020-12-28'

对于不分期线路,可采用“yyyy-mm-dd”字符串。对于分期线路,可以使用一个 Lua Table 指定各个分期的开通日期。

站名链接 编辑

本模块站名链接的例子如下

p.lines = {
	['朝阳门'] = '朝阳门站 (北京)|朝阳门',
	['北京南站'] = '北京南站|北京南站',
	['大兴线天宫院'] = {'天宫院站|天宫院', '([[北京地铁大兴线|大兴线]])'},
}

第一例中,站名“朝阳门”对应了链接 [[朝阳门站 (北京)|朝阳门]],使得调用 {{#invoke:RailSystems|stationLink|name=朝阳门|system=BJS}} 时能够返回重定向后正确的连接(而非 [[朝阳门站|朝阳门]])。

第二例中,站名“北京南站”对应的条目名为自身,因而 stationLink 返回“[[北京南站|北京南站]]”而非“[[北京南站站|北京南站]]”。需要注意的是,stationLink 根据字符串中是否存在“[[”外的“|”符号判断是否需要将字符串转换为链接。因而,单写“['北京南站'] = '北京南站',”只会返回“北京南站”的文字。

第三例中,站名“大兴线天宫院”是为了在北京地铁4号线车站条目中显示终点站为“[[天宫院站|天宫院]]([[北京地铁大兴线|大兴线]])”。这里使用了 Lua table 取代字符串。模块会自动扫描 table,将其中属于链接的部分组装为链接,并将所有部分拼装在一起。

起讫站 编辑

本模块起讫站的例子如下

p.lines['1'].terminals = { 
	left = '湘湖',
	right = { ['#field'] = 'branch',
		['#default'] = { ['#field'] = 'type', ['#default'] = '临平、文泽路', future = '临平、下沙江滨' },
		['临平'] = '临平',
		['下沙'] = { ['#field'] = 'type', ['#default'] = '文泽路', future = '下沙江滨' },
	},
}
p.lines['2'].terminals = { 
	left = '朝阳',
	right = { ['#field'] = 'type', ['#default'] = '钱江路', ['future'] = '良渚'},
}

起讫站名中,terminals 中的 left 属性表示起点站,right 属性表示终点站。两个属性的值均为迭代定义。若值为字符串,则返回站名。否则根据 '#field' 的值,判定输入参数是否符合当前 table 的某一个属性名。如果符合,取出值。否则取出 '#default' 属性的值。最后,对该值重复上述处理,直至返回结果为字符串为止。

首末班车 编辑

本模块起讫站的例子如下

p.lines['1'].trainTime = {
	-- 东环南路首班车
	['东高'] = { first = '06:00', startService = '东环南路', endService = '高桥西' },
	['东霞'] = { first = '06:00', startService = '东环南路', endService = '霞浦' },
	-- 起讫站首末班车
	['霞高'] = { first = '06:00', last = '22:00', startService = '霞浦', endService = '高桥西' },
	['高霞'] = { first = '06:00', last = '22:00', startService = '高桥西', endService = '霞浦' },
}

其中,trainTime 的每个属性值表示线路上的一种服务,名称可自定。此后 first 和 last 分别表示该服务首末班车的时间,而 startService 和 endService 表示服务的始发/终点站。

增加新功能 编辑

编者可为本模块增加新功能。但需要注意,新增的功能不应影响原有功能。为了便于维护和复用,请在完成功能编写后更新用例和帮助文档。本模块的用例位于 p.testCase,请在提交前先执行用例,确保所有用例均通过再行提交。

可用性 编辑

如果下方显示错误信息,则相应功能不可用,请联系最近一次修改者修复该错误。查看编辑历史

Category:铁路模板

-------------------------------------------------------------------
-- 重要:在保存编辑前,请务必跑完所有测试用例并保证没有错误输出。
-- 测试方法为:在调试控制台输入
--
--   print(p.testCase())
--
-- 如果输出为“所有用例均通过!”,说明可以保存编辑,否则请消除错误。
-------------------------------------------------------------------


local p = {
        DEFAULT_LINE_COLOR = 'cccccc',
        DEFAULT_STATION_SUFFIX = '站',
        DEFAULT_PLAIN_LINK_TEMPLATE = '[[{line_link}|{line_title}]]',
        DEFAULT_RICH_LINK_TEMPLATE = '&#32;<span style="color:#{line_color}"><big>■</big></span>&#32;[[{line_link}|{line_title}]]',
        DEFAULT_TRAIN_TIME_TEMPLATE = '往{station_link}方向:{train_time|H:i|次日}',
        DEFAULT_STATION_TRAIN_TIME_TEMPLATE = '{start_time|H:i|次日}-{end_time|H:i|次日}',
        DEFAULT_DATE_ELAPSED_TEXT = '[[Category:需要去除时间判断模板的页面]]',
        DEFAULT_LINE_DATE_ABSENT_TEXT = '规划中',
}

local function plain_replace(src, substr, repl)
        function literalize(str)
            return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", function(c) return "%" .. c end)
        end
        substr = literalize(substr)
        local rstr, _ = src:gsub(substr, repl)
        return rstr
end

local function _loadSystemData(system, raises)
        local system_data = nil
        local state
        if raises == nil then
                raises = true
        end
        if system ~= nil then
                state, system_data = pcall(mw.loadData, "Module:RailSystems/" .. system)
                if not state then
                        if raises then
                                error(string.format('铁道系统“%s”的数据模块不存在', system))
                        else
                                system_data = nil
                        end
                end
        end
        return system_data
end

local function _safeExpandTemplate(frame, title, args)
        local function _caller(frame, t, a)
                return frame:expandTemplate{ ['title'] = t, ['args'] = a }
        end

        local state, result = pcall(_caller, frame, title, args)
        if not state then
                return string.format('[[:Template:%s]]', title)
        else
                return result
        end
end

local function _convBool(val, default)
        if val == nil then
                return default
        elseif type(val) == 'boolean' then
                return val
        elseif mw.text.trim(val) == '' then
                return default
        end
        return (mw.text.trim(val) == '1')
end

function p._internalStationLink(name, style, short, system_data, frame)
        local station_parts, station_data
        local station_link, station_name
        local repr = ''
        if short == nil then short = true end

        local function marshalStation(station_link, station_name, style)
                if not short then
                        local link_pos, _ = string.find(station_link, ' %(')
                        if link_pos ~= nil then
                                station_name = string.sub(station_link, 1, link_pos - 1)
                        else
                                station_name = station_link
                        end
                end
                if style == nil or mw.text.trim(style) == '' then
                        return '[[' .. station_link .. '|' .. station_name .. ']]'
                else
                        return '[[' .. station_link .. '|<span style="' .. style .. '">' .. station_name .. '</span>]]'
                end
        end

        if type(system_data) == 'string' then
                system_data = _loadSystemData(system_data, false)
        end
        if system_data == nil then
                return nil
        end

        if system_data.stationNames[name] ~= nil then
                station_data = system_data.stationNames[name]
                if type(station_data) ~= 'table' then
                        station_data = {station_data, }
                end
                for i, datum in ipairs(station_data) do
                        if string.find(datum, '|', 1, true) == nil or string.find(datum, '[[', 1, true) ~= nil or string.find(datum, '{{', 1, true) ~= nil then
                                repr = repr .. datum
                        else
                                station_parts = mw.text.split(datum, '|')
                                station_link = station_parts[1]
                                station_name = table.concat(station_parts, '|', 2)
                                if frame ~= nil and string.find(station_name, '{', 1, true) ~= nil then
                                        station_name = frame:preprocess(table.concat(station_parts, '|', 2))
                                end
                                repr = repr .. marshalStation(station_link, station_name, style)
                        end
                end
                return repr
        else
                station_link = name .. p.DEFAULT_STATION_SUFFIX
                return marshalStation(station_link, name, style)
        end
end

function p._internalLineColor(code, prefix, system)
        local color_prefix = ''
        local system_data = _loadSystemData(system, false)
        if system_data == nil then
                return nil
        end

        if prefix then
                color_prefix = '#'
        end
        if system_data.lines[code] == nil then
                return color_prefix .. p.DEFAULT_LINE_COLOR
        elseif system_data.lines[code].color == nil then
                return color_prefix .. p.DEFAULT_LINE_COLOR
        else
                return color_prefix .. system_data.lines[code].color
        end
end

function p._internalLineTitle(line_code, link, system)
        local system_data = _loadSystemData(system, false)
        local tmpl = p.DEFAULT_PLAIN_LINK_TEMPLATE
        local raw_title, link_parts, link_str, _match
        if link == nil then
                link = true
        end
        if system_data == nil then
                return nil
        end
        raw_title = system_data.lines[line_code].title
        link_parts = mw.text.split(raw_title, '|')

        if string.find(raw_title, '[[', 1, true) ~= nil then
                return raw_title
        end

        if system_data.plain_link_template ~= nil then
                tmpl = system_data.plain_link_template
        end
        if link then
                if table.getn(link_parts) == 1 then
                        link_str, _match = string.gsub(string.gsub(tmpl, '\{line_link\}', link_parts[1]), '\{line_title\}', link_parts[1])
                else
                        link_str, _match = string.gsub(string.gsub(tmpl, '\{line_link\}', link_parts[1]), '\{line_title\}', link_parts[2])
                end
        else
                link_str = link_parts[1]
        end
        return link_str
end

function p._internalLineRichTitle(line_code, auto_degrade, system)
        local system_data = _loadSystemData(system)
        local raw_title = system_data.lines[line_code].title
        local line_color = system_data.lines[line_code].color
        local link_parts = mw.text.split(raw_title, '|')
        local tmpl = p.DEFAULT_RICH_LINK_TEMPLATE
        local link, _match

        if string.find(raw_title, '[[', 1, true) ~= nil then
                return raw_title
        end
        if system_data.rich_link_template ~= nil then
                tmpl = system_data.rich_link_template
        end

        if line_color == nil then
                if auto_degrade then
                        return p._internalLineTitle(line_code, true, system)
                else
                        line_color = p.DEFAULT_LINE_COLOR
                end
        end

        link, _match = string.gsub(tmpl, '\{line_link\}', link_parts[1])
        link, _match = string.gsub(link, '\{line_title\}', link_parts[2])
        link, _match = string.gsub(link, '\{line_color\}', line_color)
        return link
end

function p._internalLineTerminal(line_code, side, type_, branch, system)
        local system_data = _loadSystemData(system, false)
        local side_data, conds

        function get_condition(cond, data)
                local c
                if type(data) == 'table' then
                        c = cond[data['#field']]
                        if data[c] == nil then
                                return get_condition(cond, data['#default'])
                        else
                                return get_condition(cond, data[c])
                        end
                else
                        return data
                end
        end

        if system_data ~= nil and system_data.lines[line_code] ~= nil and system_data.lines[line_code].terminals ~= nil then
                side_data = system_data.lines[line_code].terminals[side]
                conds = { ['type'] = type_, ['branch'] = branch }
                return get_condition(conds, side_data)
        else
                return nil
        end
end

function p._parseTime(time_str)
        local base_parts = mw.text.split(time_str, ':')
        local parts_size = table.getn(base_parts)
        local hour = 0
        local minute = 0
        local sec = 0
        if parts_size == 1 then
                minute = tonumber(mw.text.trim(base_parts[1]))
        elseif parts_size == 2 then
                hour = tonumber(mw.text.trim(base_parts[1]))
                minute = tonumber(mw.text.trim(base_parts[2]))
        elseif parts_size == 3 then
                hour = tonumber(mw.text.trim(base_parts[1]))
                minute = tonumber(mw.text.trim(base_parts[2]))
                sec = tonumber(mw.text.trim(base_parts[3]))
        else
                error('非法的时间输入')
        end
        return hour, minute, sec
end

function p._extractSeconds(totalsec)
        local hour = math.floor(totalsec / 3600)
        local minute = math.floor(totalsec % 3600 / 60)
        local sec = totalsec % 60
        return hour, minute, sec
end

function p._internalTrainTime(line_code, dir, type_, delta, system)
        local system_data = _loadSystemData(system)
        local dir_info = system_data.lines[line_code].trainTime[dir]
        local bhour, bmin, bsec = p._parseTime(dir_info[type_])
        local dhour, dmin, dsec = p._parseTime(delta)
        local totalsec = (bhour * 60 + bmin) * 60 + bsec + (dhour * 60 + dmin) * 60 + dsec
        local hour, minute, sec = p._extractSeconds(totalsec)
        return dir_info.endService, hour, minute, sec
end

function p._formatTemplateTime(frame, sparts, hour, minute, second)
        local tomorrow_prefix = ''
        if table.getn(sparts) >= 3 then
                if hour >= 24 then
                        tomorrow_prefix = mw.text.trim(sparts[3])
                        if tomorrow_prefix == '' then tomorrow_prefix = '次日' end
                        hour = hour - 24
                end
        end
        local time_str = string.format('%d:%d:%d', hour, minute, second)
        return tomorrow_prefix .. frame:callParserFunction{ name = '#time', args = { sparts[2], time_str } }
end

function p._internalLineDateMessage(line_code, type_, reprs, options, system)
        local system_data = _loadSystemData(system)
        local frame = mw.getCurrentFrame()
        local lowest_level = 0 -- 0: everything is ok, 1: year, 2: month, 3: day
        local cur_time = frame:callParserFunction{ name = '#time', args = { 'U' } }
        local open_date, year, month, day, new_year, new_month
        local ret_str = ''

        for i, repr in ipairs(reprs) do
                repr = mw.text.trim(repr)
                if lowest_level < 1 and (repr == 'year') then
                        lowest_level = 1
                elseif lowest_level < 2 and (repr == 'month' or repr == 'ym') then
                        lowest_level = 2
                elseif  lowest_level < 3 and (repr == 'day' or repr == 'date' or repr == 'ymd') then
                        lowest_level = 3
                end
        end

        if options == nil then options = {} end
        options.auto_hide = _convBool(options.auto_hide, false)
        options.auto_defer = _convBool(options.auto_defer, true)
        if options.cur_time ~= nil then -- for testing purpose
                cur_time = frame:callParserFunction{ name = '#time', args = { 'U', options.cur_time } }
        end

        if system_data.lines[line_code] == nil then return nil end

        open_date = system_data.lines[line_code].openDates
        if open_date == nil then open_date = {} end
        if type(open_date) == 'table' then
                if open_date[type_] ~= nil then
                        open_date = open_date[type_]
                elseif open_date['#default'] ~= nil then
                        open_date = open_date['#default']
                elseif lowest_level == 0 then
                        for i, repr in ipairs(reprs) do ret_str = ret_str .. repr end
                        return ret_str
                else
                        return p.DEFAULT_LINE_DATE_ABSENT_TEXT
                end
        end

        year = tonumber(frame:callParserFunction{ name = '#time', args = { 'Y', open_date } })
        month = tonumber(frame:callParserFunction{ name = '#time', args = { 'm', open_date } })
        day = tonumber(frame:callParserFunction{ name = '#time', args = { 'd', open_date } })
        if options.auto_defer then
                if lowest_level == 1 then
                        open_date = string.format('%04d-01-01', year + 1)
                elseif lowest_level == 2 then
                        new_year, new_month = year, month
                        if month == 12 then
                                new_year = new_year + 1
                                new_month = 1
                        else
                                new_month = new_month + 1
                        end
                        open_date = string.format('%04d-%02d-01', new_year, new_month)
                end
        end
        open_date = frame:callParserFunction{ name = '#time', args = { 'U', open_date } }

        if options.auto_hide and cur_time >= open_date then
                return p.DEFAULT_DATE_ELAPSED_TEXT
        end
        for i, repr in ipairs(reprs) do
                if repr == 'year' then
                        ret_str = ret_str .. year .. '年'
                elseif repr == 'month' then
                        ret_str = ret_str .. month .. '月'
                elseif repr == 'day' then
                        ret_str = ret_str .. day .. '日'
                elseif repr == 'ym' then
                        ret_str = ret_str .. string.format('%s年%s月', year, month)
                elseif repr == 'date' or repr == 'ymd' then
                        ret_str = ret_str .. string.format('%s年%s月%s日', year, month, day)
                else
                        ret_str = ret_str .. repr
                end
        end
        return ret_str
end

function p.stationLink(frame)
        local a = frame.args
        local short = _convBool(a.short, true)
        local system_data = _loadSystemData(a.system, false)
        local ret = p._internalStationLink(a.name, a.style, short, system_data, frame)
        if ret == nil then
                return _safeExpandTemplate(frame, string.format('%s stations', a.system), {line=a.link, branch=a.branch, station=a.name, state=a.state})
        else
                return ret
        end
end

function p.lineColor(frame)
        local a = frame.args
        a.prefix = _convBool(a.prefix, false)

        local ret = p._internalLineColor(a.name, a.prefix, a.system)
        if ret == nil then
                ret = _safeExpandTemplate(frame, string.format('%s color', a.system), {a.name})
                if a.prefix then ret = '#' .. ret end
                return ret
        else
                return ret
        end
end

function p.lineTitle(frame)
        local a = frame.args
        local link = _convBool(a.link, true)
        local rev = p._internalLineTitle(a.name, link, a.system)
        if rev == nil then
                return _safeExpandTemplate(frame, string.format('%s lines', a.system), {a.name})
        else
                return rev
        end
end

function p.lineRichTitle(frame)
        local a = frame.args
        local degrade = _convBool(a.degrade, true)
        return p._internalLineRichTitle(a.name, degrade, a.system)
end

function p.lineTerminal(frame)
        local a = frame.args
        local term = p._internalLineTerminal(a.name, a.side, a['type'], a.branch, a.system)
        if term == nil then
                term = _safeExpandTemplate(frame, string.format('S-line/%s %s/%s', a.system, a.side, a.name), { ['type']=a['type'], branch=a.branch })
                return _safeExpandTemplate(frame, string.format('%s stations', a.system), {line=a.name, branch=a.branch, station=term, state=a.state})
        else
                return p._internalStationLink(term, '', true, a.system, frame)
        end
end

function p.lineTerminalName(frame)
        local a = frame.args
        local terminal_str = p._internalLineTerminal(a.name, a.side, a['type'], a.branch, a.system)

        if terminal_str == nil then
                return _safeExpandTemplate(frame, string.format('S-line/%s %s/%s', a.system, a.side, a.name), { ['type']=a['type'], branch=a.branch })
        else
                return terminal_str
        end
end

function p.trainTime(frame)
        local a = frame.args
        if a['type'] == 'f' or a['type'] == 'F' then a['type'] = 'first' end
        if a['type'] == 'l' or a['type'] == 'L' then a['type'] = 'last' end

        local terminal, hour, minute, sec = p._internalTrainTime(a.name, a.dir, a['type'], a.delta, a.system)
        return string.format('%02d:%02d', hour, minute)
end

function p.trainDirectionTime(frame)
        local str = frame.template
        if str == nil then str = p.DEFAULT_TRAIN_TIME_TEMPLATE end

        local a = frame.args
        if a['type'] == 'f' or a['type'] == 'F' then a['type'] = 'first' end
        if a['type'] == 'l' or a['type'] == 'L' then a['type'] = 'last' end

        local terminal, hour, minute, sec = p._internalTrainTime(a.name, a.dir, a['type'], a.delta, a.system)

        local l = 0
        while true do
                local l, r, c = string.find(str, '\{([^\}]+)\}', l + 1)
                if l == nil then break end
                local sparts = mw.text.split(c, '|')
                if sparts[1] == 'station_name' then
                        str = plain_replace(str, '{' .. c .. '}', terminal)
                elseif sparts[1] == 'station_link' then
                        str = plain_replace(str, '{' .. c .. '}', p._internalStationLink(terminal, '', true, a.system, frame))
                elseif sparts[1] == 'train_time' then
                        str = plain_replace(str, '{' .. c .. '}', p._formatTemplateTime(frame, sparts, hour, minute, sec))
                end
        end
        return str
end

function p.stationTrainTime(frame)
        local a = frame.args
        local dir_reprs = {}
        local system_data = _loadSystemData(a.system)

        local min_time_data = 999999
        local max_time_data = -999999

        local line_names = mw.text.split(a.name, ',')

        for k, v in pairs(a) do
                if type(k) == 'number' then
                        dir_reprs[k] = v
                end
        end

        for k, line_name in ipairs(line_names) do
                for i = 1, table.getn(dir_reprs), 2 do
                        local dir = dir_reprs[i]
                        local diff = dir_reprs[i + 1]
                        local dir_times = system_data.lines[line_name].trainTime[dir]

                        if dir_times ~= nil then
                                local dh, dm, ds = p._parseTime(diff)
                                for _, dir_time in ipairs({dir_times.first, dir_times.last}) do
                                        if dir_time ~= nil then
                                                local h, m, s = p._parseTime(dir_time)
                                                local t = (h * 60 + m) * 60 + s + (dh * 60 + dm) * 60 + ds
                                                min_time_data = math.min(min_time_data, t)
                                                max_time_data = math.max(max_time_data, t)
                                        end
                                end
                        end
                end
        end

        local min_h, min_m, min_s = p._extractSeconds(min_time_data)
        local max_h, max_m, max_s = p._extractSeconds(max_time_data)

        local str = frame.template
        if str == nil then str = p.DEFAULT_STATION_TRAIN_TIME_TEMPLATE end
        local l = 0
        while true do
                local l, r, c = string.find(str, '\{([^\}]+)\}', l + 1)
                if l == nil then break end
                local sparts = mw.text.split(c, '|')
                if sparts[1] == 'start_time' then
                        str = plain_replace(str, '{' .. c .. '}', p._formatTemplateTime(frame, sparts, min_h, min_m, min_s))
                elseif sparts[1] == 'end_time' then
                        str = plain_replace(str, '{' .. c .. '}', p._formatTemplateTime(frame, sparts, max_h, max_m, max_s))
                end
        end
        return str
end

function p.lineDateMessage(frame)
        local a = frame.args
        local name, type_, system = a.name, a['type'], a.system
        local reprs, options = {}, {}
        local preserved = { name = true, ['type'] = true, system = true }
        for k, v in pairs(a) do
                if preserved[k] == nil then
                        if type(k) == 'number' then
                                reprs[k] = v
                        else
                                options[k] = v
                        end
                end
        end
        return p._internalLineDateMessage(name, type_, reprs, options, system)
end

function p.renderStationLinks(frame)
        local a = frame:getParent().args
        local short = _convBool(a.short, true)
        local spos, epos, station_name = 1, 1, nil
        local pos = 1
        local system_data = _loadSystemData(a.system)
        local sys_args = { system=true, short=true }

        local text = ''
        for k, v in frame:getParent():argumentPairs() do
                if type(k) == 'number' then
                        if string.len(text) > 0 then
                                text = text .. '|' .. v
                        else
                                text = v
                        end
                else
                        if sys_args[k] == nil then
                                if string.len(text) > 0 then
                                        text = text .. '|' .. k .. '=' .. v
                                else
                                        text = k .. '=' .. v
                                end
                        end
                end
        end
        text = mw.text.trim(text)

        local out_text = ''
        while spos ~= nil do
                spos, epos, station_name = string.find(text, '%$%$([^%$]+)%$%$', pos)
                if spos ~= nil then
                        out_text = out_text .. string.sub(text, pos, spos - 1) .. p._internalStationLink(station_name, '', short, system_data, frame)
                        pos = epos + 1
                else
                        out_text = out_text .. string.sub(text, pos, string.len(text))
                end
        end
        return out_text
end

function p.testCase(frame)
        local frame = mw.getCurrentFrame()
        local lineTerminalNameFrame
        local link, _match

        local function mkfp(func_name)
                local fun = p[func_name]
                local function call_frame(frame)
                        return fun(mw.getCurrentFrame():newChild(frame))
                end
                return call_frame
        end

        local fp = {
                stationLink = mkfp('stationLink'),
                lineTitle = mkfp('lineTitle'),
                lineColor = mkfp('lineColor'),
                lineTerminal = mkfp('lineTerminal'),
                lineTerminalName = mkfp('lineTerminalName'),
                trainDirectionTime = mkfp('trainDirectionTime'),
                stationTrainTime = mkfp('stationTrainTime'),
        }

        -- stationLink test
        if fp.stationLink{args={name='宁波站', system='UseCase'}} ~= '[[宁波站|宁波站]]' then
                error('stationLink test failed: name=宁波站.')
        end
        if fp.stationLink{args={name='寧波站', system='UseCase'}} ~= '[[宁波站|宁波站]]' then
                error('stationLink test failed: name=宁波站.')
        end
        if fp.stationLink{args={name='高桥西', system='UseCase'}} ~= '[[高桥西站 (宁波)|高桥西]]' then
                error('stationLink test failed: name=高桥西.')
        end
        if fp.stationLink{args={name='高桥西', short='0', system='UseCase'}} ~= '[[高桥西站 (宁波)|高桥西站]]' then
                error('stationLink test failed: name=高桥西, short=0.')
        end
        if fp.stationLink{args={name='泽民', system='UseCase'}} ~= '[[泽民站|泽民]]' then
                error('stationLink test failed: name=泽民.')
        end
        if fp.stationLink{args={name='内嵌模板', system='UseCase'}} ~= '[[内嵌模板站|内嵌|模板]]' then
                error('stationLink test failed: name=内嵌模板.')
        end

        -- lineColor test
        if p.lineColor{args={name='1', system='UseCase'}} ~= '3180b7' then
                error('lineColor test failed: name=1.')
        end
        if p.lineColor{args={name='1', prefix='1', system='UseCase'}} ~= '#3180b7' then
                error('lineColor test failed: name=1.')
        end
        if fp.lineColor{args={name='1', system='TestComp'}} ~= 'cc0000' then
                error('lineColor compatibility test failed: name=1.')
        end
        if fp.lineColor{args={name='1', prefix='1', system='NonExist'}} ~= '#[[:Template:NonExist color]]' then
                error('lineColor compatibility test failed: name=1.')
        end

        -- lineTitle test
        if p.lineTitle{args={name='1', system='UseCase'}} ~= "'''[[宁波轨道交通1号线|1号线]]'''" then
                error('lineTitle test failed: name=1.')
        end
        if p.lineTitle{args={name='WRL', system='UseCase'}} ~= "'''[[西鐵綫|西鐵綫]]'''" then
                error('lineTitle test failed: name=WRL.')
        end
        if p.lineTitle{args={name='WRL', link='0', system='UseCase'}} ~= "西鐵綫" then
                error('lineTitle test failed: name=WRL, link=0.')
        end
        if fp.lineTitle{args={ name='1', system='TestComp'}} ~= '[[哈尔滨地铁1号线|1号线]]' then
                error('lineTitle compatibility test failed.')
        end
        if fp.lineTitle{args={ name='1', system='NonExist'}} ~= '[[:Template:NonExist lines]]' then
                error('lineTitle non compatibility test failed.')
        end

        -- lineRichTitle test
        link, _match = string.gsub(p.DEFAULT_RICH_LINK_TEMPLATE, '\{line_link\}', '宁波轨道交通1号线')
        link, _match = string.gsub(link, '\{line_title\}', '1号线')
        link, _match = string.gsub(link, '\{line_color\}', '3180b7')
        if p.lineRichTitle{args={name='1', system='UseCase'}} ~= link then
                error('lineRichTitle test failed: name=1.')
        end
        -- degrade case
        if p.lineRichTitle{args={name='S1', system='UseCase'}} ~= "'''[[宁波至余慈城际铁路|余慈线]]'''" then
                error('lineRichTitle test failed: name=1.')
        end

        -- lineTerminal test
        -- 仅此一例,其余测试覆盖由 lineTerminalName 完成
        if fp.lineTerminal{args={name='S1', side='left', type='F', system='UseCase'}} ~= '[[宁波东站|宁波东站]]' then
                error('lineTerminal test failed: name=S1, side=left, type=F.')
        end
        if fp.lineTerminal{args={ name='1', side='left', type='s1', branch='b1', system='TestComp'}} ~= '[[S1B1]]' then
                error('lineTerminal compatibility test failed: name=S1, side=left, type=F.')
        end

        -- lineTerminalName test
        if fp.lineTerminalName{args={ name='1', side='left', system='UseCase'}} ~= '高桥西' then
                error('lineTerminalName test failed: name=1, side=left.')
        end
        if fp.lineTerminalName{args={ name='S1', side='left', system='UseCase'}} ~= '宁波站' then
                error('lineTerminalName test failed: name=1, side=left.')
        end
        if fp.lineTerminalName{args={ name='S1', side='left', type='F', system='UseCase'}} ~= '宁波东站' then
                error('lineTerminalName test failed: name=1, side=left.')
        end
        if fp.lineTerminalName{args={ name='S1', side='right', system='UseCase'}} ~= '余姚' then
                error('lineTerminalName test failed: name=1, side=left.')
        end
        if fp.lineTerminalName{args={ name='S1', side='right', type='F', system='UseCase'}} ~= '马渚' then
                error('lineTerminalName test failed: name=1, side=left.')
        end
        if fp.lineTerminalName{args={ name='S1', side='right', type='F', branch='HZW', system='UseCase'}} ~= '杭州湾' then
                error('lineTerminalName test failed: name=1, side=left.')
        end

        -- lineTerminalName compatibility test
        if fp.lineTerminalName{args={ name='1', side='left', ['type']='s1', branch='b1', system='TestComp'}} ~= 'S1B1' then
                error('lineTerminalName compatibility test failed: type=s1, branch=b1.')
        end
        if fp.lineTerminalName{args={ name='1', side='left', ['type']='s1', system='TestComp'}} ~= 'S1DEFAULT' then
                error('lineTerminalName compatibility test failed: type=s1.')
        end
        if fp.lineTerminalName{args={ name='1', side='left', ['type']='s2', system='TestComp'}} ~= 'DEFAULT' then
                error('lineTerminalName compatibility test failed: type=s2.')
        end
        if fp.lineTerminalName{args={ name='1', side='left', system='NonExist'}} ~= '[[:Template:S-line/NonExist left/1]]' then
                error('lineTerminalName compatibility test failed: type=NonExist.')
        end

        -- trainTime test
        if p.trainTime{args={name='1', dir='霞高', ['type']='L', delta='0:3', system='UseCase'}} ~= '22:06' then
                error('trainTime test failed: dir=霞高, type=L, delta=0:3.')
        end
        if p.trainTime{args={name='1', dir='高霞', ['type']='F', delta='0:4', system='UseCase'}} ~= '06:08' then
                error('trainTime test failed: dir=高霞, type=F, delta=0:4.')
        end

        -- trainDirectionTime test
        if fp.trainDirectionTime{args={name='1', dir='霞高', ['type']='L', delta='0:3', system='UseCase'}} ~= "往[[高桥西站 (宁波)|高桥西]]方向:22:06" then
                error('trainDirectionTime test failed: dir=霞高, type=L, delta=0:3.')
        end
        if fp.trainDirectionTime{args={name='1', dir='高霞', ['type']='F', delta='0:4', system='UseCase'}} ~= "往[[霞浦站 (宁波)|霞浦]]方向:06:08" then
                error('trainDirectionTime test failed: dir=高霞, type=F, delta=0:4.')
        end

        -- stationTrainTime test
        if fp.stationTrainTime{args={'高霞', '0:3', name='1', system='UseCase'}} ~= "06:07-22:08" then
                error('stationTrainTime test failed: 高霞, 0:3, name=1.')
        end
        if  fp.stationTrainTime{args={'高霞', '0:3', '栎清', '0:1', name='1,2', system='UseCase'}} ~= "06:07-22:10" then
                error('stationTrainTime test failed: 高霞, 0:3, name=1,2.')
        end

        -- lineDateMessage test
        if p.lineDateMessage{args = {'在', 'year', '的', 'month', '的', 'day', ',月份', 'ym', ',日期', 'date', '开通', name='1', ['type']='2', cur_time = '2016-1-1', auto_hide=true, system='UseCase'}} ~= '在2016年的3月的19日,月份2016年3月,日期2016年3月19日开通' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'year', name='1', ['type']='2', cur_time = '2016-5-19', auto_hide=true, system='UseCase'}} ~= '2016年' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'year', name='1', ['type']='2', cur_time = '2017-3-19', auto_hide=true, system='UseCase'}} ~= p.DEFAULT_DATE_ELAPSED_TEXT then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ym', name='1', ['type']='2', cur_time = '2016-3-30', auto_hide=true, system='UseCase'}} ~= '2016年3月' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ym', name='1', ['type']='2', cur_time = '2016-4-19', auto_hide=true, system='UseCase'}} ~= p.DEFAULT_DATE_ELAPSED_TEXT then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ymd', name='1', ['type']='2', cur_time = '2016-3-18', auto_hide=true, system='UseCase'}} ~= '2016年3月19日' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ymd', name='1', ['type']='2', cur_time = '2016-3-19', auto_hide=true, system='UseCase'}} ~= p.DEFAULT_DATE_ELAPSED_TEXT then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ymd', name='S1', cur_time = '2016-3-18', auto_hide=true, system='UseCase'}} ~= '2019年12月28日' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'二期规划中', name='2', auto_hide=true, system='UseCase'}} ~= '二期规划中' then
                error('lineDateMessage test failed.')
        end
        if p.lineDateMessage{args = {'ymd', name='2', auto_hide=true, system='UseCase'}} ~= p.DEFAULT_LINE_DATE_ABSENT_TEXT then
                error('lineDateMessage test failed.')
        end

        local child_frame = frame:newChild{title='RenderStations', args={system='UseCase', '1号线从$$高桥西$$\n', '到$$霞浦$$\n'}}
        if p.renderStationLinks(child_frame:newChild{title='renderStationLinks', args={}}) ~= '1号线从[[高桥西站 (宁波)|高桥西]]\n|到[[霞浦站 (宁波)|霞浦]]' then
                error('renderStationLinks test failed: short=true.')
        end
        child_frame = frame:newChild{title='RenderStations', args={system='UseCase', short='0', '1号线从$$高桥西$$\n', '到$$霞浦$$\n'}}
        if p.renderStationLinks(child_frame:newChild{title='renderStationLinks', args={}}) ~= '1号线从[[高桥西站 (宁波)|高桥西站]]\n|到[[霞浦站 (宁波)|霞浦站]]' then
                error('renderStationLinks test failed: short=false.')
        end

        return '所有用例均通过!'
end

return p