PageSourceSearch

https://antonyliaw-housing-market-analysis.hf.space/static/js/format.js?v=3

js antonyliaw-housing-market-analysis.hf.space collected 2026-10-03 16:40:42 UTC 7,957 bytes, 213 lines download raw bytes

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.