improve date parsing for partial dates (#1683)

This commit is contained in:
jeffvli
2026-04-02 19:39:08 -07:00
parent ad11a9303c
commit 72f1d2f9f9
13 changed files with 268 additions and 202 deletions
@@ -22,9 +22,9 @@ import { AppRoute } from '/@/renderer/router/routes';
import { useShowRatings } from '/@/renderer/store';
import {
formatDateAbsolute,
formatDateAbsoluteUTC,
formatDateRelative,
formatDurationString,
formatPartialIsoDateUTC,
formatRating,
} from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
@@ -1161,12 +1161,10 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
},
{
format: (data) => {
if ('releaseYear' in data && data.releaseYear !== null) {
if ('releaseYear' in data && data.releaseYear != null) {
const releaseYear = data.releaseYear;
const originalYear =
'originalYear' in data && data.originalYear !== null
? data.originalYear
: null;
'originalYear' in data && data.originalYear > 0 ? data.originalYear : null;
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
@@ -1186,10 +1184,10 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
data.originalDate &&
data.originalDate !== data.releaseDate
) {
return `${formatDateAbsoluteUTC(data.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(data.releaseDate)}`;
return `${formatPartialIsoDateUTC(data.originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(data.releaseDate)}`;
}
return `${formatDateAbsoluteUTC(data.releaseDate)}`;
return `${formatPartialIsoDateUTC(data.releaseDate)}`;
}
return '';
},
@@ -1,6 +1,21 @@
import { ItemDetailListCellProps } from './types';
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
import { formatPartialIsoDateUTC } from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <>&nbsp;</>;
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) => {
const row = song as typeof song & { originalDate?: null | string };
const releaseDate = row.releaseDate;
if (!releaseDate) {
return <>&nbsp;</>;
}
const originalDate =
row.originalDate && row.originalDate !== releaseDate ? row.originalDate : null;
if (originalDate) {
return `${formatPartialIsoDateUTC(originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(releaseDate)}`;
}
return formatPartialIsoDateUTC(releaseDate);
};
@@ -67,7 +67,7 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes';
import { useSettingsStore, useShowRatings } from '/@/renderer/store';
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
import { formatDurationString, formatPartialIsoDateUTC } from '/@/renderer/utils';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
@@ -489,9 +489,9 @@ const MetadataSection = memo(
let releaseStr = '';
if (item.releaseDate) {
if (item.originalDate && item.originalDate !== item.releaseDate) {
releaseStr = `${formatDateAbsoluteUTC(item.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(item.releaseDate)}`;
releaseStr = `${formatPartialIsoDateUTC(item.originalDate)}${SEPARATOR_STRING}${formatPartialIsoDateUTC(item.releaseDate)}`;
} else {
releaseStr = formatDateAbsoluteUTC(item.releaseDate);
releaseStr = formatPartialIsoDateUTC(item.releaseDate);
}
} else if (item.releaseYear != null) {
releaseStr = String(item.releaseYear);
@@ -8,49 +8,20 @@ import {
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import {
formatDateAbsolute,
formatDateAbsoluteUTC,
formatDateRelative,
formatHrDateTime,
formatPartialIsoDateUTC,
} from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { TableColumn } from '/@/shared/types/types';
const getDateTooltipLabel = (utcString: string) => {
return (
<Stack gap="xs" justify="center">
<Text size="md" ta="center">
{formatHrDateTime(utcString)}
</Text>
<Text isMuted size="sm" ta="center">
{utcString}
</Text>
</Stack>
);
};
const DateColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
const { formattedDate, tooltipLabel } = useMemo(() => {
if (typeof row === 'string' && row) {
return {
formattedDate: formatDateAbsolute(row),
tooltipLabel: getDateTooltipLabel(row),
};
}
return { formattedDate: null, tooltipLabel: null };
}, [row]);
if (typeof row === 'string' && row) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
<span>{formatDateAbsolute(row)}</span>
</TableColumnTextContainer>
);
}
@@ -79,44 +50,32 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
: null;
if (originalDate) {
const formattedOriginalDate = formatDateAbsoluteUTC(originalDate);
const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate);
const displayText = `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`;
return {
displayText,
tooltipLabel: getDateTooltipLabel(releaseDate),
};
const formattedOriginalDate = formatPartialIsoDateUTC(originalDate);
const formattedReleaseDate = formatPartialIsoDateUTC(releaseDate);
return `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`;
}
if (typeof releaseDate === 'string' && releaseDate) {
return {
displayText: formatDateAbsoluteUTC(releaseDate),
tooltipLabel: getDateTooltipLabel(releaseDate),
};
return formatPartialIsoDateUTC(releaseDate);
}
}
}
return null;
}, [props.type, rowItem]);
const { formattedDate, tooltipLabel } = useMemo(() => {
if (typeof row === 'string' && row) {
return {
formattedDate: formatDateAbsoluteUTC(row),
tooltipLabel: getDateTooltipLabel(row),
};
}
return { formattedDate: null, tooltipLabel: null };
}, [row]);
if (props.type === TableColumn.RELEASE_DATE) {
if (releaseDateContent) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={releaseDateContent.tooltipLabel} multiline={false}>
<span>{releaseDateContent.displayText}</span>
</Tooltip>
<span>{releaseDateContent}</span>
</TableColumnTextContainer>
);
}
if (typeof row === 'string' && row) {
return (
<TableColumnTextContainer {...props}>
<span>{formatPartialIsoDateUTC(row)}</span>
</TableColumnTextContainer>
);
}
@@ -128,20 +87,6 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
}
if (typeof row === 'string' && row) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonFixed {...props} />;
};
@@ -151,22 +96,10 @@ const RelativeDateColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
const { formattedDate, tooltipLabel } = useMemo(() => {
if (typeof row === 'string') {
return {
formattedDate: formatDateRelative(row),
tooltipLabel: getDateTooltipLabel(row),
};
}
return { formattedDate: null, tooltipLabel: null };
}, [row]);
if (typeof row === 'string') {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
<span>{formatDateRelative(row)}</span>
</TableColumnTextContainer>
);
}
@@ -13,10 +13,10 @@ const YearColumnBase = (props: ItemTableListInnerColumn) => {
const item = rowItem as any;
const yearDisplay = useMemo(() => {
if (item && 'releaseYear' in item && item.releaseYear !== null) {
if (item && 'releaseYear' in item && item.releaseYear != null) {
const releaseYear = item.releaseYear;
const originalYear =
'originalYear' in item && item.originalYear !== null ? item.originalYear : null;
'originalYear' in item && item.originalYear > 0 ? item.originalYear : null;
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
@@ -20,7 +20,7 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useShowRatings } from '/@/renderer/store';
import { useArtistRadioCount, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDateAbsoluteUTC, formatDurationString, formatSizeString } from '/@/renderer/utils';
import { formatDurationString, formatPartialIsoDateUTC, formatSizeString } from '/@/renderer/utils';
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
import { Group } from '/@/shared/components/group/group';
import { Separator } from '/@/shared/components/separator/separator';
@@ -131,7 +131,10 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
const originalDifferentFromRelease =
album?.originalDate && album?.originalDate !== album?.releaseDate;
const originalYearDifferentFromRelease = album?.originalYear !== album?.releaseYear;
const originalYearDifferentFromRelease =
album.originalYear > 0 &&
album.releaseYear != null &&
album.originalYear !== album.releaseYear;
const playCount = album?.playCount;
@@ -147,17 +150,17 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
if (originalDifferentFromRelease) {
items.push({
id: 'originalDate',
value: `${formatDateAbsoluteUTC(album.originalDate)}`,
value: `${formatPartialIsoDateUTC(album.originalDate)}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
value: `${releasePrefix} ${formatPartialIsoDateUTC(releaseDate)}`,
});
}
} else if (album.originalYear) {
} else if (album.originalYear > 0) {
if (originalYearDifferentFromRelease) {
items.push({
id: 'originalYear',
@@ -168,14 +171,24 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releaseYearPrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
value: `${releaseYearPrefix} ${formatPartialIsoDateUTC(releaseDate)}`,
});
} else if (releaseYear) {
} else if (releaseYear != null && releaseYear > 0) {
items.push({
id: 'releaseYear',
value: `${releaseYearPrefix} ${releaseYear}`,
});
}
} else if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${formatPartialIsoDateUTC(releaseDate)}`,
});
} else if (releaseYear != null && releaseYear > 0) {
items.push({
id: 'releaseYear',
value: `${releaseYear}`,
});
}
items.push(
+1 -1
View File
@@ -36,7 +36,7 @@ export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
mbzReleaseGroupId: null,
name: song.album ?? '',
originalDate: null,
originalYear: null,
originalYear: 0,
participants: song.participants,
playCount: null,
recordLabels: [],
+48
View File
@@ -76,6 +76,16 @@ const getDayjsLocale = (i18nLang: string): string => {
return localeMap[i18nLang] || 'en';
};
// BCP 47 tags for Intl (differs from dayjs locale ids for some languages).
const getIntlLocale = (i18nLang: string): string => {
const localeMap: Record<string, string> = {
'zh-Hans': 'zh-CN',
'zh-Hant': 'zh-TW',
};
return localeMap[i18nLang] ?? i18nLang;
};
const updateDayjsLocale = () => {
const dayjsLocale = getDayjsLocale(i18n.language);
dayjs.locale(dayjsLocale);
@@ -92,6 +102,44 @@ export const formatDateAbsolute = (key: null | string) => (key ? dayjs(key).form
export const formatDateAbsoluteUTC = (key: null | string) =>
key ? dayjs.utc(key).format('ll') : '';
const PARTIAL_ISO_YEAR = /^\d{4}$/;
const PARTIAL_ISO_YEAR_MONTH = /^\d{4}-\d{2}$/;
export const formatPartialIsoDateUTC = (key: null | string): string => {
if (!key) {
return '';
}
const trimmedKey = key.trim();
const intlLocale = getIntlLocale(i18n.language);
if (PARTIAL_ISO_YEAR.test(trimmedKey)) {
const year = Number.parseInt(trimmedKey, 10);
if (!Number.isFinite(year)) {
return trimmedKey;
}
return new Intl.DateTimeFormat(intlLocale, { timeZone: 'UTC', year: 'numeric' }).format(
new Date(Date.UTC(year, 0, 1)),
);
}
if (PARTIAL_ISO_YEAR_MONTH.test(trimmedKey)) {
const d = dayjs.utc(`${trimmedKey}-01`);
if (!d.isValid()) {
return trimmedKey;
}
return new Intl.DateTimeFormat(intlLocale, {
month: 'long',
timeZone: 'UTC',
year: 'numeric',
}).format(d.toDate());
}
return dayjs.utc(trimmedKey).format('ll');
};
export const formatHrDateTime = (key: null | string) => (key ? dayjs(key).format('lll') : '');
export const formatDateRelative = (key: null | string) => (key ? dayjs(key).fromNow() : '');