Skip to content
Draft
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
5 changes: 5 additions & 0 deletions deno.lock

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

1 change: 1 addition & 0 deletions deps/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "jsr:@takker/[email protected]";
158 changes: 1 addition & 157 deletions task.ts
Original file line number Diff line number Diff line change
@@ -1,157 +1 @@
import { addSeconds, isAfter, lightFormat } from "./deps/date-fns.ts";
import { isNone, isString } from "./utils.ts";
import { getIndentLineCount } from "./deps/scrapbox-std.ts";

export type Interval = {
start?: Date;
end?: Date;
};
/** ミリ秒単位の経過時間 */
export type Duration = number;
/** task data */
export interface Task {
title: string;
base: Date;
plan: {
start?: Date;
/** 見積もり時間 単位は秒 */ duration?: number;
};
record: Interval;
}

const parseTask = (text: string): Task | undefined => {
const matched = text.match(
/^`(\d{4})-(\d{2})-(\d{2}) (?: {5}|(\d{2}):(\d{2})) (?: {4}|(\d{4})) (?: {8}|(\d{2}):(\d{2}):(\d{2})) (?: {8}|(\d{2}):(\d{2}):(\d{2}))`([^\n]*)$/,
);
if (!matched) return;

// タスクが書き込まれた行を解析する
const [
,
year,
month,
date,
phours,
pminutes,
duration,
shours,
sminutes,
sseconds,
ehours,
eminutes,
eseconds,
title,
] = matched;
const task: Task = {
title,
base: new Date(parseInt(year), parseInt(month) - 1, parseInt(date)),
plan: {},
record: {},
};

if (phours) {
const start = new Date(task.base);
start.setHours(parseInt(phours));
start.setMinutes(parseInt(pminutes));
task.plan.start = start;
}
if (duration) task.plan.duration = parseInt(duration) * 60;
// 実績時刻を解析する
// 開始時刻より終了時刻の方が前だったら、日付を越えているとみなす
if (shours) {
const start = new Date(task.base);
start.setHours(parseInt(shours));
start.setMinutes(parseInt(sminutes));
start.setSeconds(parseInt(sseconds));
task.record.start = start;
}
if (ehours) {
const end = new Date(task.base);
end.setHours(parseInt(ehours));
end.setMinutes(parseInt(eminutes));
end.setSeconds(parseInt(eseconds));
if (task.record.start && isAfter(task.record.start, end)) {
end.setDate(end.getDate() + 1);
}
task.record.end = end;
}

return task;
};
export { parseTask as parse };
export const isTask = (text: string): boolean => parseTask(text) !== undefined;

/** 比較用の開始日時を取得する */
export const startDate = (task: Task): Date =>
task.record?.start ?? task.plan?.start ?? task.base;

/** 比較用の終了を取得する */
export const endDate = (task: Task): Date =>
task.record?.end ??
(!isNone(task.plan?.duration)
? addSeconds(startDate(task), task.plan.duration)
: task.base);

/** 実行中か判定する */
export const isRunning = (task: Task): boolean =>
task.record.start !== undefined && task.record.end === undefined;

/** 完了したか判定する */
export const isDone = (task: Task): boolean =>
task.record.start !== undefined && task.record.end !== undefined;

/** Taskを文字列に直す */
export const toString = ({ title, base, plan, record }: Task): string =>
[
"`",
lightFormat(base, "yyyy-MM-dd"),
" ",
plan?.start ? lightFormat(plan.start, "HH:mm") : " ".repeat(5),
" ",
plan?.duration
? `${plan.duration / 60}`
.padStart(4, "0")
: " ".repeat(4),
" ",
record?.start ? lightFormat(record?.start, "HH:mm:ss") : " ".repeat(8),
" ",
record?.end ? lightFormat(record?.end, "HH:mm:ss") : " ".repeat(8),
"`",
title,
].join("");

/** Taskに、インデントでぶら下がっている行のテキストデータを加えたもの*/
export interface TaskBlock extends Task {
/**ぶら下がっているテキストデータ */ lines: string[];
}
/** 本文データから、タスクとそこにぶら下がった行をまとめて返す */
export function* parseBlock(
lines: { text: string }[] | string[],
): Generator<TaskBlock, void, void> {
for (const data of parseLines(lines)) {
if (isString(data)) continue;
yield data;
}
}
/** 本文データを解析して結果を返す */
export function* parseLines(
lines: { text: string }[] | string[],
): Generator<TaskBlock | string, void, void> {
for (let i = 0; i < lines.length; i++) {
const line_ = lines[i];
const line = isString(line_) ? line_ : line_.text;
const count = getIndentLineCount(i, lines);
const task = parseTask(line);
if (!task) {
yield line;
continue;
}
yield {
...task,
lines: lines.slice(i + 1, i + 1 + count).map((line_) =>
isString(line_) ? line_ : line_.text
),
};
i += count;
}
}
export * from "./v1/task.ts";
2 changes: 1 addition & 1 deletion task.test.ts → v1/task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Task,
toString,
} from "./task.ts";
import { assert, assertEquals } from "./deps/testing.ts";
import { assert, assertEquals } from "../deps/testing.ts";

const testData: [string, Task][] = [
[
Expand Down
179 changes: 179 additions & 0 deletions v1/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { addSeconds, isAfter, lightFormat, set } from "../deps/date-fns.ts";
import { isNone, isString } from "../utils.ts";
import { getIndentLineCount } from "../deps/scrapbox-std.ts";
import {
all,
and,
map,
match,
or,
parse,
Parser,
skip,
text,
} from "../deps/parser.ts";

export type Interval = {
start?: Date;
end?: Date;
};
/** ミリ秒単位の経過時間 */
export type Duration = number;
/** task data */
export interface Task {
title: string;
base: Date;
plan: {
start?: Date;
/** 見積もり時間 単位は秒 */ duration?: number;
};
record: Interval;
}

const backquote = text("`");
const hyphen = text("-");
const colon = text(":");
const nnnn = map(match(/\d{4}/), parseInt);
const nn = map(match(/\d{2}/), parseInt);
const date = map(
all(nnnn, hyphen, nn, hyphen, nn),
([year, , month, , date]) => new Date(year, month - 1, date),
);
export const hhmm: Parser<[hour: number, minutes: number], string> = and(
skip(nn, colon),
nn,
);
const hhmmss: Parser<[hour: number, minutes: number, seconds: number], string> =
all(
skip(nn, colon),
skip(nn, colon),
nn,
);
const space = text(" ");
const space4 = map(text(" "), () => undefined);
const space5 = map(text(" "), () => undefined);
const space8 = map(text(" "), () => undefined);
const task = map(
all(
backquote,
date,
space,
or(hhmm, space5),
space,
or(nnnn, space4),
space,
or(hhmmss, space8),
space,
or(hhmmss, space8),
backquote,
match(/[^\n]*$/),
),
([, base, , begin, , duration, , start, , end, , title]): Task => {
const record: Interval = {};
if (start) {
record.start = set(base, {
hours: start[0],
minutes: start[1],
seconds: start[2],
});
}
if (record.start && end) {
record.end = set(base, {
hours: end[0],
minutes: end[1],
seconds: end[2],
});
if (isAfter(record.start, record.end)) {
record.end.setDate(record.end.getDate() + 1);
}
}
const plan: { start?: Date; duration?: number } = {};
if (begin) plan.start = set(base, { hours: begin[0], minutes: begin[1] });
if (duration) plan.duration = duration * 60;

return { title, base, plan, record };
},
);

const parseTask = (text: string): Task | undefined => {
const result = parse(task, text);
return result.ok ? result.value : undefined;
};

export { parseTask as parse };
export const isTask = (text: string): boolean => parseTask(text) !== undefined;

/** 比較用の開始日時を取得する */
export const startDate = (task: Task): Date =>
task.record?.start ?? task.plan?.start ?? task.base;

/** 比較用の終了を取得する */
export const endDate = (task: Task): Date =>
task.record?.end ??
(!isNone(task.plan?.duration)
? addSeconds(startDate(task), task.plan.duration)
: task.base);

/** 実行中か判定する */
export const isRunning = (task: Task): boolean =>
task.record.start !== undefined && task.record.end === undefined;

/** 完了したか判定する */
export const isDone = (task: Task): boolean =>
task.record.start !== undefined && task.record.end !== undefined;

/** Taskを文字列に直す */
export const toString = ({ title, base, plan, record }: Task): string =>
[
"`",
lightFormat(base, "yyyy-MM-dd"),
" ",
plan?.start ? lightFormat(plan.start, "HH:mm") : " ".repeat(5),
" ",
plan?.duration
? `${plan.duration / 60}`
.padStart(4, "0")
: " ".repeat(4),
" ",
record?.start ? lightFormat(record?.start, "HH:mm:ss") : " ".repeat(8),
" ",
record?.end ? lightFormat(record?.end, "HH:mm:ss") : " ".repeat(8),
"`",
title,
].join("");

/** Taskに、インデントでぶら下がっている行のテキストデータを加えたもの*/
export interface TaskBlock extends Task {
/**ぶら下がっているテキストデータ */ lines: string[];
}
/** 本文データから、タスクとそこにぶら下がった行をまとめて返す */
export function* parseBlock(
lines: { text: string }[] | string[],
): Generator<TaskBlock, void, void> {
for (const data of parseLines(lines)) {
if (isString(data)) continue;
yield data;
}
}
/** 本文データを解析して結果を返す */
export function* parseLines(
lines: { text: string }[] | string[],
): Generator<TaskBlock | string, void, void> {
for (let i = 0; i < lines.length; i++) {
const line_ = lines[i];
const line = isString(line_) ? line_ : line_.text;
const count = getIndentLineCount(i, lines);
const task = parseTask(line);
if (!task) {
yield line;
continue;
}
yield {
...task,
lines: lines.slice(i + 1, i + 1 + count).map((line_) =>
isString(line_) ? line_ : line_.text
),
};
i += count;
}
}