Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions clis/xueqiu/earnings-date.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cli } from '@jackwener/opencli/registry';
import { EmptyResultError } from '@jackwener/opencli/errors';
import { fetchXueqiuJson } from './utils.js';
import { fetchXueqiuJson, formatChinaDate } from './utils.js';
cli({
site: 'xueqiu',
name: 'earnings-date',
Expand Down Expand Up @@ -32,7 +32,7 @@ cli({
.filter((item) => item.subtype === 2)
.map((item) => {
const ts = item.timestamp;
const dateStr = ts ? new Date(ts).toISOString().split('T')[0] : null;
const dateStr = ts ? formatChinaDate(ts) : null;
const isFuture = ts && ts > now;
return { date: dateStr, report: item.message, status: isFuture ? '⏳ 未发布' : '✅ 已发布', _ts: ts, _future: isFuture };
});
Expand Down
4 changes: 2 additions & 2 deletions clis/xueqiu/kline.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cli } from '@jackwener/opencli/registry';
import { EmptyResultError } from '@jackwener/opencli/errors';
import { fetchXueqiuJson } from './utils.js';
import { fetchXueqiuJson, formatChinaDate } from './utils.js';
cli({
site: 'xueqiu',
name: 'kline',
Expand Down Expand Up @@ -31,7 +31,7 @@ cli({
const colIdx = {};
columns.forEach((name, i) => { colIdx[name] = i; });
return d.data.item.map(row => ({
date: colIdx.timestamp != null ? new Date(row[colIdx.timestamp]).toISOString().split('T')[0] : null,
date: colIdx.timestamp != null ? formatChinaDate(row[colIdx.timestamp]) : null,
open: row[colIdx.open] ?? null,
high: row[colIdx.high] ?? null,
low: row[colIdx.low] ?? null,
Expand Down
19 changes: 19 additions & 0 deletions clis/xueqiu/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';

const CHINA_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
});

/** Format a Unix ms timestamp as the matching `YYYY-MM-DD` in Asia/Shanghai (xueqiu's canonical user timezone for all markets). */
export function formatChinaDate(ts) {
if (ts == null) return null;
const parts = Object.fromEntries(
CHINA_DATE_FORMATTER.formatToParts(new Date(ts))
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value]),
);
return `${parts.year}-${parts.month}-${parts.day}`;
}

/**
* Fetch a xueqiu JSON API from inside the browser context (credentials included).
* Page must already be navigated to xueqiu.com before calling this function.
Expand Down
26 changes: 26 additions & 0 deletions clis/xueqiu/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { formatChinaDate } from './utils.js';

describe('formatChinaDate', () => {
it('returns the Asia/Shanghai date for a UTC ms at China midnight', () => {
expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08');
});
it('returns the same China date for a moment late in the day', () => {
expect(formatChinaDate(Date.UTC(2026, 4, 8, 14, 0, 0))).toBe('2026-05-08');
});
it('formats representative A-share and US-market bars on xueqiu Beijing dates', () => {
expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08');
expect(formatChinaDate(Date.UTC(2026, 4, 10, 16, 0, 0))).toBe('2026-05-11');
});
it('crosses the China day boundary at 16:00 UTC', () => {
expect(formatChinaDate(Date.UTC(2026, 0, 1, 15, 59, 59))).toBe('2026-01-01');
expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toBe('2026-01-02');
});
it('always returns an ISO calendar date string, not a locale-shaped slash date', () => {
expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it('returns null for nullish input', () => {
expect(formatChinaDate(null)).toBeNull();
expect(formatChinaDate(undefined)).toBeNull();
});
});
Loading