160 lines
3.9 KiB
TypeScript
160 lines
3.9 KiB
TypeScript
import type { ProductPlanningPlatform } from "./types";
|
||
|
||
const HEADER_SANITIZE_PATTERN = /[\s_\-./\\()[\]{}::,,;;'"`~!@#$%^&*+|?<>]/g;
|
||
|
||
export function normalizeHeader(value: unknown) {
|
||
return String(value || "")
|
||
.replace(/^\ufeff/, "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(HEADER_SANITIZE_PATTERN, "");
|
||
}
|
||
|
||
export function normalizeText(value: unknown) {
|
||
return String(value || "").replace(/^\ufeff/, "").trim();
|
||
}
|
||
|
||
export function parseRangeValue(value: unknown): number | null {
|
||
if (value === null || value === undefined || value === "") {
|
||
return null;
|
||
}
|
||
|
||
const text = String(value).trim();
|
||
if (!text) return null;
|
||
|
||
const rangeMatch = text.match(
|
||
/^(\d+(?:\.\d+)?)\s*[-~~至到]\s*(\d+(?:\.\d+)?)$/
|
||
);
|
||
if (rangeMatch) {
|
||
return Math.min(Number.parseFloat(rangeMatch[1]), Number.parseFloat(rangeMatch[2]));
|
||
}
|
||
|
||
const cleaned = text.replace(/[,,%%\s]/g, "");
|
||
const parsed = Number.parseFloat(cleaned);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
|
||
export function parseInteger(value: unknown, fallback = 0) {
|
||
const parsed = Number.parseInt(String(value || "").trim(), 10);
|
||
return Number.isFinite(parsed) ? parsed : fallback;
|
||
}
|
||
|
||
export function ensureHttpsUrl(value: unknown) {
|
||
const text = String(value || "").trim();
|
||
if (!text) return "";
|
||
if (text.startsWith("//")) return `https:${text}`;
|
||
if (/^https?:\/\//i.test(text)) return text;
|
||
return `https://${text}`;
|
||
}
|
||
|
||
export function splitUrlCell(value: unknown) {
|
||
const text = String(value || "").trim();
|
||
if (!text) return [] as string[];
|
||
|
||
return text
|
||
.split(/[\n,,;;\s]+/)
|
||
.map((item) => ensureHttpsUrl(item))
|
||
.filter((item) => /^https?:\/\//i.test(item));
|
||
}
|
||
|
||
export function extractTaobaoItemId(value: string) {
|
||
const input = String(value || "").trim();
|
||
if (!input) return null;
|
||
|
||
if (/^\d+$/.test(input)) return input;
|
||
|
||
const patterns = [
|
||
/item\.taobao\.com\/item\.htm.*?[?&]id=(\d+)/i,
|
||
/detail\.tmall\.com\/item\.htm.*?[?&]id=(\d+)/i,
|
||
/a\.m\.taobao\.com\/i(\d+)/i,
|
||
/[?&]id=(\d+)/i,
|
||
/(\d{10,})/,
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
const matched = input.match(pattern);
|
||
if (matched?.[1]) {
|
||
return matched[1];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function extractJdItemId(value: string) {
|
||
const input = String(value || "").trim();
|
||
if (!input) return null;
|
||
|
||
const explicitPrefix = input.match(/^jd[\s::]+(\d{6,})$/i);
|
||
if (explicitPrefix?.[1]) {
|
||
return explicitPrefix[1];
|
||
}
|
||
|
||
const patterns = [
|
||
/item\.jd\.com\/(\d+)\.html/i,
|
||
/sku=(\d+)/i,
|
||
/\/product\/(\d+)\.html/i,
|
||
/[?&]sku=(\d+)/i,
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
const matched = input.match(pattern);
|
||
if (matched?.[1]) {
|
||
return matched[1];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function parseMarketplaceItemReference(value: string): {
|
||
platform: ProductPlanningPlatform;
|
||
itemId: string;
|
||
} | null {
|
||
const input = String(value || "").trim();
|
||
if (!input) return null;
|
||
|
||
const jdItemId = extractJdItemId(input);
|
||
if (jdItemId) {
|
||
return {
|
||
platform: "jd",
|
||
itemId: jdItemId,
|
||
};
|
||
}
|
||
|
||
const taobaoItemId = extractTaobaoItemId(input);
|
||
if (taobaoItemId) {
|
||
return {
|
||
platform: "taobao",
|
||
itemId: taobaoItemId,
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export function uniqueBy<T>(items: T[], getKey: (item: T) => string) {
|
||
const seen = new Set<string>();
|
||
const result: T[] = [];
|
||
|
||
for (const item of items) {
|
||
const key = getKey(item);
|
||
if (!key || seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
export function truncateText(value: unknown, maxLength: number) {
|
||
const text = normalizeText(value);
|
||
if (text.length <= maxLength) return text;
|
||
return `${text.slice(0, maxLength)}...`;
|
||
}
|
||
|
||
export function parseOfficialFlag(value: unknown) {
|
||
const text = normalizeText(value).toLowerCase();
|
||
return ["true", "1", "yes", "y", "是", "官方"].includes(text);
|
||
}
|