UNPKG

rsshub

Version:
122 lines (121 loc) 5.62 kB
//#region lib/routes/temposmart/utils.ts const TSUBO_M2 = 3.30579; /** Collapse whitespace from cheerio `.text()`; empty → null. */ const clean = (text) => { const s = (text ?? "").replaceAll(/\s+/g, " ").trim(); return s === "" ? null : s; }; const isUnknown = (text) => /^(?:[--―—]|ご?相談|要相談|応相談|未定|非公開)?$/.test(text); /** * '352,000円(税込)' → 352000;'160万円' → 1600000;'330,000' → 330000; * 'なし' / '無償譲渡' → 0;'ご相談' / '-' / '' → null。 */ const parseJpy = (text) => { if (text === null) return null; const s = text.replaceAll(/[,,\s]/g, ""); if (/無償|なし|無し/.test(s)) return 0; if (isUnknown(s)) return null; if (!/[円万億]/.test(s) && !/^\d+(?:\.\d+)?$/.test(s)) return null; const oku = s.match(/^(\d+(?:\.\d+)?)億(?:(\d+(?:\.\d+)?)万)?/); if (oku) return Math.round(Number(oku[1]) * 1e8 + (oku[2] ? Number(oku[2]) * 1e4 : 0)); const m = s.match(/\d+(?:\.\d+)?/); if (!m) return null; const n = Number(m[0]); return Math.round(s.includes("万") ? n * 1e4 : n); }; const round2 = (n) => Math.round(n * 100) / 100; /** '21.42坪(70.84㎡)' / '70㎡' → { tsubo, area_m2 }, the missing side converted at 3.30579. */ const parseArea = (text) => { if (text === null) return { tsubo: null, area_m2: null }; const s = text.replaceAll(/[,,]/g, ""); const tsubo = s.match(/(\d+(?:\.\d+)?)\s*坪/)?.[1]; const m2 = s.match(/(\d+(?:\.\d+)?)\s*(?:㎡|m²|m2|平米)/)?.[1]; return { tsubo: tsubo ? Number(tsubo) : m2 ? round2(Number(m2) / TSUBO_M2) : null, area_m2: m2 ? Number(m2) : tsubo ? round2(Number(tsubo) * TSUBO_M2) : null }; }; /** 'N ヶ月 / ヵ月 / か月 / カ月' → N;'なし' → 0;'ご相談' / '' → null。 */ const parseMonths = (text) => { if (text === null) return null; const s = text.replaceAll(/\s/g, ""); if (/なし|無し/.test(s)) return 0; const m = s.match(/(\d+(?:\.\d+)?)[ヶヵかカケ]月/); return m?.[1] ? Number(m[1]) : null; }; /** '10ヶ月 / なし' (保証金 / 敷金) → sum of the known parts; all unknown → null. */ const parseMonthsSum = (text) => { if (text === null) return null; const known = text.split(/[//]/).map((p) => parseMonths(clean(p))).filter((p) => p !== null); return known.length === 0 ? null : known.reduce((a, b) => a + b, 0); }; /** '徒歩6分' / '3分' → 6 / 3。 */ const parseWalkMin = (text) => { const m = text?.match(/(\d+)\s*分/); return m?.[1] ? Number(m[1]) : null; }; /** '3階' → '3F','B1階' → 'B1F','1〜2階' → '1F〜2F'。 */ const normalizeFloor = (text) => { const tokens = text?.match(/(?:B|-)?\d+/gi); if (!tokens) return null; return tokens.map((t) => /^(?:B|-)/i.test(t) ? `B${t.slice(1)}F` : `${t}F`).join("〜"); }; /** '2026年09月10日' / '2026/09/11' → '2026-09-10'。 */ const parseYmd = (text) => { const m = text?.match(/(\d{4})[./年-](\d{1,2})[./月-](\d{1,2})/); if (!m?.[1] || !m[2] || !m[3]) return null; return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`; }; /** '東京都港区麻布十番二丁目' → '港区';'藤沢市鵠沼石上1丁目' → '藤沢市'。 */ const parseWard = (address) => { if (address === null) return null; const s = address.replace(/^(?:東京都|北海道|(?:京都|大阪)府|\S{2,3}県)/, ""); for (const re of [ /^(.+?区)/, /^(.+?市)/, /^(.+?郡)/, /^(.+?(?:町|村))/ ]) { const m = s.match(re); if (m?.[1]) return m[1]; } return null; }; /** 現況:居抜き → inuki;スケルトン → skeleton;otherwise null. */ const parseCondition = (...texts) => { const s = texts.filter((t) => t !== null).join(" "); if (/居抜き|居抜/.test(s)) return "inuki"; if (s.includes("スケルトン")) return "skeleton"; return null; }; /** '重飲食可' → true;'重飲食不可' / '飲食不可' → false;plain '飲食可' → null. */ const parseHeavyFood = (...texts) => { const s = texts.filter((t) => t !== null).join(" "); if (/重飲食(?:・[^\s【】/]+)?\s*(?:不可|NG|×)|飲食\s*不可/.test(s)) return false; if (/重飲食\s*(?:可|OK|○)/.test(s)) return true; return null; }; /** '… 【何業も可】【重飲食不可】' → ['何業も可', '重飲食不可']。 */ const bracketNotes = (text) => (text ?? "").matchAll(/【([^】]+)】/g).toArray().map((m) => m[1].trim()).filter(Boolean); /** 賃料 ÷ 坪 when the site does not give 坪単価. */ const tsuboUnit = (rentJpy, tsubo) => rentJpy !== null && tsubo !== null && tsubo > 0 ? Math.round(rentJpy / tsubo) : null; const sumKnown = (a, b) => { if (a === null && b === null) return null; return (a ?? 0) + (b ?? 0); }; /** Structured one-line summary used as the item description. */ const summarize = (x) => [ x.rent_jpy === null ? null : `賃料 ${x.rent_jpy.toLocaleString("ja-JP")}円`, x.tsubo === null ? null : `${x.tsubo}坪`, x.floor, x.station === null ? null : `${x.station}駅 徒歩${x.walk_min ?? "?"}分`, x.condition === "inuki" ? "居抜き" : x.condition === "skeleton" ? "スケルトン" : null, x.prev_business === null ? null : `現況 ${x.prev_business}`, x.fixtures_transfer_jpy === null ? null : `造作 ${x.fixtures_transfer_jpy === 0 ? "無償" : `${x.fixtures_transfer_jpy.toLocaleString("ja-JP")}円`}`, x.ward ].filter((p) => p !== null).join(" / "); //#endregion export { parseCondition as a, parseMonths as c, parseWard as d, parseYmd as f, tsuboUnit as h, parseArea as i, parseMonthsSum as l, summarize as m, clean as n, parseHeavyFood as o, sumKnown as p, normalizeFloor as r, parseJpy as s, bracketNotes as t, parseWalkMin as u };