Files
red-valley/resources/[framework]/[addons]/jg-dealerships/shared/search-helpers.lua
2026-03-29 21:41:17 +03:00

254 lines
9.1 KiB
Lua

--[[
Description:
Shared utility functions for server-side search functionality.
Includes smart date parsing for SQL datetime columns.
Global Namespace:
SearchHelpers
Globals:
SearchHelpers.BuildDateCondition - Builds SQL condition for smart date searching
SearchHelpers.ParseSearchTermForDate - Parses search term and returns date-related SQL conditions
Exports:
None
]]--
SearchHelpers = SearchHelpers or {}
-- Month name mappings (English only)
local MONTH_NAMES = {
["january"] = 1, ["jan"] = 1,
["february"] = 2, ["feb"] = 2,
["march"] = 3, ["mar"] = 3,
["april"] = 4, ["apr"] = 4,
["may"] = 5,
["june"] = 6, ["jun"] = 6,
["july"] = 7, ["jul"] = 7,
["august"] = 8, ["aug"] = 8,
["september"] = 9, ["sep"] = 9, ["sept"] = 9,
["october"] = 10, ["oct"] = 10,
["november"] = 11, ["nov"] = 11,
["december"] = 12, ["dec"] = 12
}
---@param searchTerm string
---@return number|nil month
local function parseMonthName(searchTerm)
local lower = searchTerm:lower()
return MONTH_NAMES[lower]
end
---@param searchTerm string
---@return number|nil year
local function parseYear(searchTerm)
local year = tonumber(searchTerm)
if year and year >= 1900 and year <= 2100 then
return year
end
return nil
end
---@param searchTerm string
---@param dateColumn string The SQL column name for the datetime field
---@return string|nil condition, table|nil params
function SearchHelpers.BuildDateCondition(searchTerm, dateColumn)
if not searchTerm or searchTerm == "" then return nil, nil end
local trimmed = Trim(searchTerm)
local lower = trimmed:lower()
local conditions = {}
local params = {}
-- Check for exact year (e.g., "2024")
local year = parseYear(trimmed)
if year then
conditions[#conditions+1] = ("YEAR(%s) = ?"):format(dateColumn)
params[#params+1] = year
return conditions[1], params
end
-- Check for month name only (e.g., "November", "Nov")
local month = parseMonthName(trimmed)
if month then
conditions[#conditions+1] = ("MONTH(%s) = ?"):format(dateColumn)
params[#params+1] = month
return conditions[1], params
end
-- Check for "Month Year" format (e.g., "November 2024", "Nov 2024")
local monthYearMatch = trimmed:match("^(%a+)%s+(%d+)$")
if monthYearMatch then
local monthPart, yearPart = trimmed:match("^(%a+)%s+(%d+)$")
local parsedMonth = parseMonthName(monthPart)
local parsedYear = parseYear(yearPart)
if parsedMonth and parsedYear then
conditions[#conditions+1] = ("(MONTH(%s) = ? AND YEAR(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = parsedMonth
params[#params+1] = parsedYear
return conditions[1], params
end
end
-- Check for "Year Month" format (e.g., "2024 November", "2024 Nov")
local yearMonthMatch = trimmed:match("^(%d+)%s+(%a+)$")
if yearMonthMatch then
local yearPart, monthPart = trimmed:match("^(%d+)%s+(%a+)$")
local parsedMonth = parseMonthName(monthPart)
local parsedYear = parseYear(yearPart)
if parsedMonth and parsedYear then
conditions[#conditions+1] = ("(MONTH(%s) = ? AND YEAR(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = parsedMonth
params[#params+1] = parsedYear
return conditions[1], params
end
end
-- Check for "YYYY-MM" format (e.g., "2024-11")
local yearDashMonth = trimmed:match("^(%d%d%d%d)%-(%d%d?)$")
if yearDashMonth then
local y, m = trimmed:match("^(%d%d%d%d)%-(%d%d?)$")
local parsedYear = tonumber(y)
local parsedMonth = tonumber(m)
if parsedYear and parsedMonth and parsedMonth >= 1 and parsedMonth <= 12 then
conditions[#conditions+1] = ("(MONTH(%s) = ? AND YEAR(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = parsedMonth
params[#params+1] = parsedYear
return conditions[1], params
end
end
-- Check for "MM/YYYY" or "MM-YYYY" format
local monthSlashYear = trimmed:match("^(%d%d?)[/%-](%d%d%d%d)$")
if monthSlashYear then
local m, y = trimmed:match("^(%d%d?)[/%-](%d%d%d%d)$")
local parsedMonth = tonumber(m)
local parsedYear = tonumber(y)
if parsedYear and parsedMonth and parsedMonth >= 1 and parsedMonth <= 12 then
conditions[#conditions+1] = ("(MONTH(%s) = ? AND YEAR(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = parsedMonth
params[#params+1] = parsedYear
return conditions[1], params
end
end
-- Check for exact date formats: YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY
-- YYYY-MM-DD
local ymdMatch = trimmed:match("^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$")
if ymdMatch then
local y, m, d = trimmed:match("^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$")
conditions[#conditions+1] = ("DATE(%s) = ?"):format(dateColumn)
params[#params+1] = ("%04d-%02d-%02d"):format(tonumber(y), tonumber(m), tonumber(d))
return conditions[1], params
end
-- DD/MM/YYYY or MM/DD/YYYY with full year
local dateSlashMatch = trimmed:match("^(%d%d?)/(%d%d?)/(%d%d%d%d)$")
if dateSlashMatch then
local first, second, year = trimmed:match("^(%d%d?)/(%d%d?)/(%d%d%d%d)$")
local firstNum, secondNum = tonumber(first), tonumber(second)
-- Try to intelligently detect format: if first > 12, it must be day (DD/MM/YYYY)
-- if second > 12, it must be day (MM/DD/YYYY), otherwise try both
local day, month
if firstNum > 12 then
-- Must be DD/MM/YYYY
day, month = firstNum, secondNum
elseif secondNum > 12 then
-- Must be MM/DD/YYYY
month, day = firstNum, secondNum
else
-- Ambiguous - try both formats with OR condition
conditions[#conditions+1] = ("(DATE(%s) = ? OR DATE(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = ("%04d-%02d-%02d"):format(tonumber(year), secondNum, firstNum) -- DD/MM/YYYY
params[#params+1] = ("%04d-%02d-%02d"):format(tonumber(year), firstNum, secondNum) -- MM/DD/YYYY
return conditions[1], params
end
conditions[#conditions+1] = ("DATE(%s) = ?"):format(dateColumn)
params[#params+1] = ("%04d-%02d-%02d"):format(tonumber(year), month, day)
return conditions[1], params
end
-- DD/MM/YY or MM/DD/YY with short year (2 digits)
local dateSlashShortYear = trimmed:match("^(%d%d?)/(%d%d?)/(%d%d)$")
if dateSlashShortYear then
local first, second, shortYear = trimmed:match("^(%d%d?)/(%d%d?)/(%d%d)$")
local firstNum, secondNum = tonumber(first), tonumber(second)
-- Convert 2-digit year to 4-digit (assume 2000s for 00-99)
local fullYear = 2000 + tonumber(shortYear)
local day, month
if firstNum > 12 then
-- Must be DD/MM/YY
day, month = firstNum, secondNum
elseif secondNum > 12 then
-- Must be MM/DD/YY
month, day = firstNum, secondNum
else
-- Ambiguous - try both formats with OR condition
conditions[#conditions+1] = ("(DATE(%s) = ? OR DATE(%s) = ?)"):format(dateColumn, dateColumn)
params[#params+1] = ("%04d-%02d-%02d"):format(fullYear, secondNum, firstNum) -- DD/MM/YY
params[#params+1] = ("%04d-%02d-%02d"):format(fullYear, firstNum, secondNum) -- MM/DD/YY
return conditions[1], params
end
conditions[#conditions+1] = ("DATE(%s) = ?"):format(dateColumn)
params[#params+1] = ("%04d-%02d-%02d"):format(fullYear, month, day)
return conditions[1], params
end
return nil, nil
end
---@param searchTerm string The user's search query
---@param textColumns table Array of column names to search with LIKE
---@param dateColumn string|nil Optional datetime column for smart date search
---@param concatenatedColumns table|nil Optional array of column groups to concatenate and search (e.g., {{"brand", "model"}})
---@return string whereClause, table params
function SearchHelpers.BuildSearchConditions(searchTerm, textColumns, dateColumn, concatenatedColumns)
if not searchTerm or searchTerm == "" then
return "", {}
end
local trimmed = Trim(searchTerm)
local conditions = {}
local params = {}
-- Build text search conditions for individual columns (case-insensitive)
local likePattern = "%" .. trimmed:lower() .. "%"
for _, col in ipairs(textColumns) do
conditions[#conditions+1] = ("LOWER(%s) LIKE ?"):format(col)
params[#params+1] = likePattern
end
-- Build concatenated column search conditions (e.g., brand + ' ' + model) (case-insensitive)
if concatenatedColumns then
for _, colGroup in ipairs(concatenatedColumns) do
if #colGroup > 1 then
local concatParts = {}
for _, col in ipairs(colGroup) do
concatParts[#concatParts+1] = col
end
local concatExpr = "CONCAT_WS(' ', " .. table.concat(concatParts, ", ") .. ")"
conditions[#conditions+1] = ("LOWER(%s) LIKE ?"):format(concatExpr)
params[#params+1] = likePattern
end
end
end
-- Add date condition if applicable
if dateColumn then
local dateCondition, dateParams = SearchHelpers.BuildDateCondition(trimmed, dateColumn)
if dateCondition and dateParams then
conditions[#conditions+1] = dateCondition
for _, p in ipairs(dateParams) do
params[#params+1] = p
end
end
end
if #conditions == 0 then
return "", {}
end
return "(" .. table.concat(conditions, " OR ") .. ")", params
end