Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.
Open
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
19 changes: 18 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0",
"builtin-modules": "^3.3.0",
"dayjs": "^1.11.11",
"esbuild": "^0.16.17",
"esbuild-jest": "^0.5.0",
"esbuild-plugin-replace": "^1.4.0",
Expand All @@ -56,11 +55,12 @@
"svelte-i18next": "^2.2.2",
"svelte-media-queries": "^1.6.2",
"svelte-preprocess": "^5.1.4",
"temporal-polyfill": "^0.2.5",
"ts-essentials": "^10.0.3",
"ts-jest": "^29.1.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"uuid": "^11.0.3",
"yaml": "^2.4.2"
}
}
}
41 changes: 32 additions & 9 deletions src/lib/dataApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dayjs from "dayjs";
import { produce } from "immer";
import moment from "moment";
import { get } from "svelte/store";
Expand All @@ -17,6 +16,7 @@ import { decodeFrontMatter, encodeFrontMatter } from "./metadata";
import { i18n } from "./stores/i18n";
import { settings } from "./stores/settings";
import { interpolateTemplate } from "./templates/interpolate";
import { Temporal } from "temporal-polyfill";

import { function as F, task as T, either as E, taskEither as TE } from "fp-ts";
import {
Expand Down Expand Up @@ -181,21 +181,44 @@ export function doUpdateRecord(
Object.entries({ ...frontmatter, ...record.values })
.map((entry) => {
if (isDate(entry[1])) {
const isDatetime = fields.find(
const [, dateValue] = entry;

const hasTime = fields.some(
(field) =>
field.name === entry[0] &&
field.type === DataFieldType.Date &&
(field.typeConfig?.time ||
entry[1].getHours() ||
entry[1].getMinutes() ||
entry[1].getSeconds() ||
entry[1].getMilliseconds())
dateValue.hour ||
dateValue.minute ||
dateValue.second ||
dateValue.millisecond)
);

const isZoned =
dateValue.timeZoneId !== Temporal.Now.timeZoneId() &&
dateValue.offset !== Temporal.Now.zonedDateTimeISO().offset;

return produce(entry, (draft) => {
draft[1] = dayjs(entry[1]).format(
isDatetime ? "YYYY-MM-DDTHH:mm" : "YYYY-MM-DD"
);
if (hasTime && isZoned) {
draft[1] =
dateValue.timeZoneId === "UTC" // The original raw string ends with "Z"
? dateValue.toString({
smallestUnit: "minute",
offset: "never",
timeZoneName: "never",
}) + "Z"
: dateValue.toString({
smallestUnit: "minute",
offset: "auto",
timeZoneName: "never",
});
} else {
draft[1] = hasTime
? dateValue
.toPlainDateTime()
.toString({ smallestUnit: "minute" })
: (draft[1] = dateValue.toPlainDate().toString());
}
});
}
return entry;
Expand Down
55 changes: 29 additions & 26 deletions src/lib/dataframe/dataframe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FieldConfig } from "src/settings/settings";
import type { RecordError } from "../datasources/frontmatter/datasource";
import { Temporal } from "temporal-polyfill";

/**
* DataFrame is the core data structure that contains structured data for a
Expand Down Expand Up @@ -77,32 +78,32 @@ export type DataValue =
| string
| number
| boolean
| Date
| Temporal.ZonedDateTime
| Array<Optional<DataValue>>;

export function isOptionalDataValue(
value: unknown
): value is Optional<DataValue> {
switch (typeof value) {
case "string":
return true;
case "number":
return true;
case "boolean":
return true;
default:
return false;
}
}
// export function isOptionalDataValue(
// value: unknown
// ): value is Optional<DataValue> {
// switch (typeof value) {
// case "string":
// return true;
// case "number":
// return true;
// case "boolean":
// return true;
// default:
// return false;
// }
// }

export function isRepeatedDataValue(
value: unknown
): value is Array<Optional<DataValue>> {
if (Array.isArray(value)) {
return value.every(isOptionalDataValue);
}
return false;
}
// export function isRepeatedDataValue(
// value: unknown
// ): value is Array<Optional<DataValue>> {
// if (Array.isArray(value)) {
// return value.every(isOptionalDataValue);
// }
// return false;
// }

export type Optional<T> =
| T
Expand Down Expand Up @@ -138,8 +139,10 @@ export function isNumber(
return typeof value === "number";
}

export function isDate(value: Optional<DataValue> | DataValue): value is Date {
return value instanceof Date;
export function isDate(
value: Optional<DataValue> | DataValue
): value is Temporal.ZonedDateTime {
return value instanceof Temporal.ZonedDateTime;
}

// export function hasValue(value: Optional<DataValue>): value is DataValue {
Expand Down Expand Up @@ -179,7 +182,7 @@ export function isOptionalNumber(

export function isOptionalDate(
value: Optional<DataValue>
): value is Optional<Date> {
): value is Optional<Temporal.ZonedDateTime> {
return isDate(value) || isOptional(value);
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/datasources/dataview/standardize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import dayjs from "dayjs";
import type { Link } from "obsidian-dataview";
import type { DataValue, Optional } from "src/lib/dataframe/dataframe";
import { Temporal } from "temporal-polyfill";

/**
* standardizeValues converts a Dataview data structure of values to the common
Expand Down Expand Up @@ -36,6 +36,6 @@ function standardizeObject(value: any) {
return (value as Link).toString();
}
if ("ts" in value) {
return dayjs(value.ts).format("YYYY-MM-DD");
return Temporal.PlainDateTime.from(value.c).toString()
}
}
2 changes: 2 additions & 0 deletions src/lib/datasources/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ describe("detectCellType", () => {
// Complex values.
["2022-01-01", DataFieldType.Date],
["2022-01-01T22:35", DataFieldType.Date],
["2022-01-01T22:35Z", DataFieldType.Date],
["2022-01-01T22:35+02:00", DataFieldType.Date],
[{ my: "object" }, DataFieldType.Unknown],
];

Expand Down
54 changes: 47 additions & 7 deletions src/lib/datasources/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import dayjs from "dayjs";

import {
DataFieldType,
type DataField,
type DataRecord,
type DataValue,
type Optional,
} from "../dataframe/dataframe";
import { Temporal } from "temporal-polyfill";

/**
* Parses the values for each record based on the detected field types.
Expand All @@ -31,7 +30,9 @@ export function parseRecords(
switch (field.type) {
case DataFieldType.Date:
if (typeof value === "string") {
record.values[field.name] = dayjs(value).toDate();
record.values[field.name] = parseStringDate(value);
} else if (typeof value === "number") {
record.values[field.name] = parseNumberDate(value);
}
break;
case DataFieldType.Number:
Expand All @@ -46,7 +47,7 @@ export function parseRecords(
break;
case DataFieldType.String:
if (typeof value !== "object") {
record.values[field.name] = value?.toLocaleString();
record.values[field.name] = value?.toLocaleString(); //TODO: fallback type, to be refined in type casting
}
break;
}
Expand All @@ -55,6 +56,44 @@ export function parseRecords(
return records;
}

function parseStringDate(value: string) {
for (const parseFn of [
() => Temporal.ZonedDateTime.from(value), // with timezone id
() => Temporal.Instant.from(value).toZonedDateTimeISO(value), // with timezone offset
() =>
Temporal.PlainDateTime.from(value).toZonedDateTime(
Temporal.Now.timeZoneId()
), // w/o timezone info
]) {
try {
return parseFn();
} catch {
continue;
}
}
return null;
}

function parseNumberDate(value: number) {
for (const parseFn of [
() =>
Temporal.PlainDateTime.from(value.toString()).toZonedDateTime(
Temporal.Now.timeZoneId()
),
() =>
Temporal.Instant.fromEpochMilliseconds(value).toZonedDateTimeISO(
Temporal.Now.timeZoneId()
),
]) {
try {
return parseFn();
} catch {
continue;
}
}
return null;
}

/**
* Merges a new version of `values` into a copy of data record.
*
Expand Down Expand Up @@ -119,12 +158,13 @@ function typeFromValues(values: Optional<DataValue>[]): DataFieldType {
}
}

export const DateTimeRegex =
/^\d{4}-\d{2}-\d{2}(?:[Tt ](?:\d{2})?(?::\d{2})?(?::\d{2})?(?:.\d+)?(?:[+-]\d{2}(?::?\d{2})?|[Zz])?(?:\[[^\]]+\])?)?$/;

export function detectCellType(value: unknown): DataFieldType {
// Standard types
if (typeof value === "string") {
if (
/^\d{4}-\d{2}-\d{2}(T)?(\d{2})?(:\d{2})?(:\d{2})?(.\d{3})?$/.test(value)
) {
if (DateTimeRegex.test(value)) {
return DataFieldType.Date;
}
return DataFieldType.String;
Expand Down
6 changes: 0 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import dayjs from "dayjs";
import isoWeek from "dayjs/plugin/isoWeek";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { either, task, taskEither } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import { Plugin, TFolder, WorkspaceLeaf, addIcon } from "obsidian";
Expand All @@ -25,9 +22,6 @@ import {
} from "./settings/settings";
import { ProjectsView, VIEW_TYPE_PROJECTS } from "./view";

dayjs.extend(isoWeek);
dayjs.extend(localizedFormat);

const PROJECTS_PLUGIN_ID = "obsidian-projects";

export default class ProjectsPlugin extends Plugin {
Expand Down
Loading