1// format.js â æ¸åãæ¥æãè®åéæ ¼å¼åå ±ç¨å½å¼ 2 3export function fmt(n) { 4 return n == null || isNaN(n) ? 'â' : n.toLocaleString('zh-TW'); 5} 6 7export function fmtPrice(n) { 8 if (n == null || isNaN(n)) return 'â'; 9 return fmt(Math.round(n / 10000)) + ' '; 10} 11 12export function fmtPriceCompact(n) { 13 if (n == null || isNaN(n)) return 'â'; 14 return fmt(Math.round(n / 10000)); 15} 16 17export function fmtDate(s) { 18 if (!s) return ''; 19 const d = new Date(s); 20 const y = d.getFullYear(); 21 const m = String(d.getMonth() + 1).padStart(2, '0'); 22 const day = String(d.getDate()).padStart(2, '0'); 23 return `${y}.${m}.${day}`; 24} 25 26export function fmtYearMonth(s) { 27 if (!s) return ''; 28 const d = new Date(s); 29 const y = d.getFullYear(); 30 const m = String(d.getMonth() + 1).padStart(2, '0'); 31 return `${y}.${m}`; 32} 33 34export function daysAgo(n, refDate) { 35 const d = new Date(refDate || new Date()); 36 d.setDate(d.getDate() - n); 37 return d.toISOString().slice(0, 10); 38} 39 40export function computeDateRange(filter, refDate) { 41 if (!filter) return { dateFrom: '', dateTo: refDate || '' }; 42 if (filter.type === 'days') { 43 return { 44 dateFrom: daysAgo(filter.days || 90, refDate), 45 dateTo: refDate || '' 46 }; 47 } 48 if (filter.type === 'custom') { 49 return { 50 dateFrom: filter.from || '', 51 dateTo: filter.to || '' 52 }; 53 } 54 return { 55 dateFrom: '', 56 dateTo: refDate || '' 57 }; 58} 59 60export function changeBadge(diff, pct) { 61 if (diff == null || diff === 0) return ''; 62 const cls = diff > 0 ? 'up' : 'down'; 63 const arrow = diff > 0 ? 'â²' : 'â¼'; 64 const pctStr = pct != null ? `(${pct}%)` : '(â%)'; 65 return `<span class="summary-change ${cls}"><span class="arrow">${arrow}</span> ${fmt(Math.abs(diff))}<br><span class="pct-str">${pctStr}</span></span>`; 66} 67 68export function fmtPriceChange(diff, pct) { 69 if (diff == null || diff === 0) return ''; 70 const cls = diff > 0 ? 'up' : 'down'; 71 const arrow = diff > 0 ? 'â²' : 'â¼'; 72 const pctStr = pct != null ? `(${pct}%)` : '(â%)'; 73 return `<span class="summary-change ${cls}"><span class="arrow">${arrow}</span> ${Math.round(Math.abs(diff) / 10000)}è¬<br><span class="pct-str">${pctStr}</span></span>`; 74} 75 76export function changeBadgeOnly(diff) { 77 if (diff == null || diff === 0) return ''; 78 const cls = diff > 0 ? 'up' : 'down'; 79 const arrow = diff > 0 ? 'â²' : 'â¼'; 80 return `<span class="summary-change ${cls}"><span class="arrow">${arrow}</span> ${fmt(Math.abs(diff))}</span>`; 81} 82 83export function fmtPriceChangeOnly(diff) { 84 if (diff == null || diff === 0) return ''; 85 const cls = diff > 0 ? 'up' : 'down'; 86 const arrow = diff > 0 ? 'â²' : 'â¼'; 87 return `<span class="summary-change ${cls}"><span class="arrow">${arrow}</span> ${Math.round(Math.abs(diff) / 10000)}è¬</span>`;
88} 89 90// ç§å±æè¦å¡è®åéï¼å ï¼ååä½ï¼ 91export function fmtRentChangeOnly(diff) { 92 if (diff == null || diff === 0) return ''; 93 const cls = diff > 0 ? 'up' : 'down'; 94 const arrow = diff > 0 ? 'â²' : 'â¼'; 95 return `<span class="summary-change ${cls}"><span class="arrow">${arrow}</span> ${fmt(Math.round(Math.abs(diff)))}å </span>`; 96} 97 98export function rowChangeBadge(diff) { 99 if (diff == null || diff === 0) return ''; 100 const cls = diff > 0 ? 'up' : 'down'; 101 const arrow = diff > 0 ? 'â²' : 'â¼'; 102 return `<span class="row-change ${cls}"><span class="arrow">${arrow}</span> ${fmt(Math.abs(diff))}</span>`; 103} 104 105export function rowPriceChangeBadge(diff) { 106 if (diff == null || diff === 0) return ''; 107 const cls = diff > 0 ? 'up' : 'down'; 108 const arrow = diff > 0 ? 'â²' : 'â¼'; 109 return `<span class="row-change ${cls}"><span class="arrow">${arrow}</span> ${Math.round(Math.abs(diff) / 10000)}è¬</span>`; 110} 111 112// ç§å±æå表è®åéï¼å ï¼ååä½ï¼ 113export function rowRentChangeBadge(diff) { 114 if (diff == null || diff === 0) return ''; 115 const cls = diff > 0 ? 'up' : 'down'; 116 const arrow = diff > 0 ? 'â²' : 'â¼'; 117 return `<span class="row-change ${cls}"><span class="arrow">${arrow}</span> ${fmt(Math.round(Math.abs(diff)))}å </span>`; 118} 119 120export const M2_TO_PING = 1 / 3.30579; 121 122export function toPing(m2) { 123 if (m2 == null || isNaN(m2)) return 'â'; 124 return (m2 * M2_TO_PING).toFixed(2); 125} 126 127export function fmtUnitPrice(totalPrice, parkingPrice, buildingArea) { 128 const bldPing = (buildingArea || 0) * M2_TO_PING; 129 if (!totalPrice || bldPing <= 0) return 'â'; 130 const netPrice = totalPrice - (parkingPrice || 0); 131 if (netPrice <= 0) return 'â'; 132 const pricePerPing = netPrice / bldPing; 133 return (pricePerPing / 10000).toFixed(2); 134} 135 136// ç§å±æ¯åªç§éï¼å /åªï¼ï¼ç§é ÷ åªæ¸ãåªæ¸å建ç©ï¼>0ï¼å¦ååå°ã 137export function fmtRentUnitPrice(totalPrice, buildingArea, landArea) { 138 const areaM2 = (buildingArea || 0) > 0 ? buildingArea : (landArea || 0); 139 const areaPing = areaM2 * M2_TO_PING; 140 if (!totalPrice || areaPing <= 0) return 'â'; 141 return fmt(Math.round(totalPrice / areaPing)); 142} 143 144export function calcAge(completeDate) { 145 if (!completeDate) return 'â'; 146 const complete = new Date(completeDate); 147 const now = new Date(); 148 const age = now.getFullYear() - complete.getFullYear(); 149 if (age < 0) return 'â'; 150 const months = (now.getFullYear() - complete.getFullYear()) * 12 + (now.getMonth() - complete.getMonth()); 151 return (months / 12).toFixed(1); 152} 153 154const CN_DIGITS = { 'ä¸': 1, 'äº': 2, 'ä¸': 3, 'å': 4, 'äº': 5, 'å ': 6, 'ä¸': 7, 'å «': 8, 'ä¹': 9 }; 155const CN_UNITS = { 'å': 10, 'ç¾': 100, 'å': 1000 }; 156 157function cnToArabic(s) { 158 let total = 0, cur = 0; 159 for (const ch of s) { 160 if (ch in CN_DIGITS) cur = CN_DIGITS[ch]; 161 else if (ch in CN_UNITS) { 162 if (cur === 0) cur = 1; 163 total += cur * CN_UNITS[ch]; 164 cur = 0; 165 } 166 } 167 return total + cur; 168} 169 170export function normalizeAddress(addr) { 171 if (!addr) return addr; 172 // å ¨å½¢æ¸å â åå½¢ 173 let s = addr.replace(/[\uFF10-\uFF19]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0)); 174 // 䏿æ¸åï¼ç·æ¥çµæ§å®ä½ï¼æ®µ/å··/å¼/è/æ¨/層/è·¯/è¡ï¼â åå½¢æ¸å 175 s = s.replace(/([ä¸äºä¸åäºå ä¸å «ä¹åç¾åé¶]+)(å°æ®µ|段|å··|å¼|è|æ¨|層|è·¯|è¡)/g, (m, num, unit) => cnToArabic(num) + unit); 176 // ãä¹ãå¾ä¹ä¸ææ¸åï¼æ¶èï¼å¦ãä¹äºãï¼â åå½¢æ¸å 177 s = s.replace(/ä¹([ä¸äºä¸åäºå ä¸å «ä¹åç¾åé¶]+)/g, (m, num) => 'ä¹' + cnToArabic(num)); 178 return s; 179} 180 181// æé¤å°åéé ç縣å¸èé鮿åï¼ä¾è¡åè£ç½®ç·æ¹é±è®ï¼ 182export function stripAdminPrefix(addr, county, town) { 183 if (!addr) return addr; 184 let s = String(addr).trim(); 185 186 const c = (county || '').trim(); 187 const t = (town || '').trim(); 188 189 // 1. åªå å試ç¶å縣å¸+éé®éé ï¼å å«ãèºãèãå°ãç°é«åç¸å®¹ï¼ 190 if (c && t) { 191 if (s.startsWith(c + t)) return s.slice((c + t).length); 192 const cAlt = c.includes('èº') ? c.replace(/èº/g, 'å°') : c.replace(/å°/g, 'èº'); 193 if (s.startsWith(cAlt + t)) return s.slice((cAlt + t).length); 194 } 195 196 // 2. å試å®ç¨ç¸£å¸æå®ç¨éé®éé 197 if (c) { 198 const cAlt = c.includes('èº') ? c.replace(/èº/g, 'å°') : c.replace(/å°/g, 'èº'); 199 if (s.startsWith(c)) s = s.slice(c.length); 200 else if (s.startsWith(cAlt)) s = s.slice(cAlt.length); 201 } 202 if (t && s.startsWith(t)) { 203 s = s.slice(t.length); 204 } 205 206 // 3. éç¨è¡æ¿ååç¶´å¥é¢ï¼å¦ãXX縣/å¸YYé/é®/å¸/åãæå®ä¸ãYYé/é®/å¸/åãï¼ 207 const fullMatch = s.match(/^(?:[\u4e00-\u9fa5]{2,4}[縣å¸])?(?:[\u4e00-\u9fa5]{2,4}[éé®å¸å])(.+)$/); 208 if (fullMatch && fullMatch[1]) { 209 return fullMatch[1]; 210 } 211 212 return s; 213}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.