From 6d95a44dbde6f1b587158cd00289449051896adb Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:01:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(parser):=20=E6=AD=A3=E8=A6=8F?= =?UTF-8?q?=E8=A1=A8=E7=8F=BE=E3=81=A7=E3=81=AF=E3=81=AA=E3=81=8F`@takker/?= =?UTF-8?q?parser`=E3=81=A7parse=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 書式変更の下準備 --- deno.lock | 5 + task.ts | 158 +----------------------------- task.test.ts => v1/task.test.ts | 2 +- v1/task.ts | 167 ++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 158 deletions(-) rename task.test.ts => v1/task.test.ts (99%) create mode 100644 v1/task.ts diff --git a/deno.lock b/deno.lock index 96cf333..78bfbfa 100644 --- a/deno.lock +++ b/deno.lock @@ -23,6 +23,8 @@ "jsr:@takker/cosense-storage@0.3": "0.3.1", "jsr:@takker/debug-js@0.1": "0.1.2", "jsr:@takker/md5@0.1": "0.1.0", + "jsr:@takker/parser@0.1": "0.1.2", + "jsr:@takker/parser@0.1.2": "0.1.2", "npm:date-fns@4": "4.1.0", "npm:esbuild@0.11": "0.11.23", "npm:gas-entry-generator@~2.5.1": "2.5.1", @@ -128,6 +130,9 @@ }, "@takker/md5@0.1.0": { "integrity": "4c423d8247aadf7bcb1eb83c727bf28c05c21906e916517395d00aa157b6eae0" + }, + "@takker/parser@0.1.2": { + "integrity": "c75818713aa21bef6200ff710d8d3acee8a334cf71ffb75195495e67ee8ffb98" } }, "npm": { diff --git a/task.ts b/task.ts index 1d90fcb..b7ab3a9 100644 --- a/task.ts +++ b/task.ts @@ -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 { - for (const data of parseLines(lines)) { - if (isString(data)) continue; - yield data; - } -} -/** 本文データを解析して結果を返す */ -export function* parseLines( - lines: { text: string }[] | string[], -): Generator { - 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"; diff --git a/task.test.ts b/v1/task.test.ts similarity index 99% rename from task.test.ts rename to v1/task.test.ts index 6e70984..2a01740 100644 --- a/task.test.ts +++ b/v1/task.test.ts @@ -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][] = [ [ diff --git a/v1/task.ts b/v1/task.ts new file mode 100644 index 0000000..e4c4860 --- /dev/null +++ b/v1/task.ts @@ -0,0 +1,167 @@ +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, map, match, or, parse, text } from "jsr:@takker/parser@0.1"; + +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), +); +const hhmm = map( + all(nn, colon, nn), + ([hour, , minutes]) => [hour, minutes] as const, +); +const hhmmss = map( + all(hhmm, colon, nn), + ([[hour, minutes], , seconds]) => [hour, minutes, seconds] as const, +); +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 { + for (const data of parseLines(lines)) { + if (isString(data)) continue; + yield data; + } +} +/** 本文データを解析して結果を返す */ +export function* parseLines( + lines: { text: string }[] | string[], +): Generator { + 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; + } +} From 0956bd43232999b43b4cc69d5687a5638cceddf6 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Tue, 20 May 2025 18:38:41 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20combinators=E3=81=AE=E7=B5=84?= =?UTF-8?q?=E3=81=BF=E5=90=88=E3=82=8F=E3=81=9B=E3=82=92=E5=B0=91=E3=81=97?= =?UTF-8?q?=E5=A4=89=E3=81=88=E3=81=9F=E3=81=A0=E3=81=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- v1/task.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/v1/task.ts b/v1/task.ts index e4c4860..abacdf5 100644 --- a/v1/task.ts +++ b/v1/task.ts @@ -1,7 +1,17 @@ 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, map, match, or, parse, text } from "jsr:@takker/parser@0.1"; +import { + all, + and, + map, + match, + or, + parse, + Parser, + skip, + text, +} from "jsr:@takker/parser@0.1"; export type Interval = { start?: Date; @@ -29,14 +39,16 @@ const date = map( all(nnnn, hyphen, nn, hyphen, nn), ([year, , month, , date]) => new Date(year, month - 1, date), ); -const hhmm = map( - all(nn, colon, nn), - ([hour, , minutes]) => [hour, minutes] as const, -); -const hhmmss = map( - all(hhmm, colon, nn), - ([[hour, minutes], , seconds]) => [hour, minutes, seconds] as const, +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); From da617e17e0c91d5af63de3f2607b4f0e884dde9a Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Tue, 20 May 2025 18:41:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20`@takker/parser`=E3=82=92`./dep?= =?UTF-8?q?s/parser.ts`=E3=81=8B=E3=82=89=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=82=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deps/parser.ts | 1 + v1/task.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 deps/parser.ts diff --git a/deps/parser.ts b/deps/parser.ts new file mode 100644 index 0000000..3142d17 --- /dev/null +++ b/deps/parser.ts @@ -0,0 +1 @@ +export * from "jsr:@takker/parser@0.1"; diff --git a/v1/task.ts b/v1/task.ts index abacdf5..d95950d 100644 --- a/v1/task.ts +++ b/v1/task.ts @@ -11,7 +11,7 @@ import { Parser, skip, text, -} from "jsr:@takker/parser@0.1"; +} from "../deps/parser.ts"; export type Interval = { start?: Date;