From 951c41424b02487aa08f871b7f7df2644167f885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=87=95=E5=AD=90=E5=A5=BD?= Date: Sun, 17 May 2026 14:19:23 +0800 Subject: [PATCH] =?UTF-8?q?[0022]=20=E4=BC=98=E5=8C=96=20pyfmt=20=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devel/0022.md | 24 +++++++++++++++ goldfish/liii/string.scm | 53 +++++++++++++++++--------------- tests/liii/string/pyfmt-test.scm | 17 ++++++++++ 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/devel/0022.md b/devel/0022.md index 4e837f16..a358c7d6 100644 --- a/devel/0022.md +++ b/devel/0022.md @@ -13,6 +13,30 @@ bin/gf test ``` +## 2026/05/16 优化 pyfmt 边界行为 + +### What +1. 修复 `pyfmt` 将 `#f` 字段值误判为缺失字段的问题。 +2. 修复缺失字段时 `%(key)s` / `%(key)d` 占位符被截断的问题。 +3. 为字符串 key、`#f` 字段值、缺失字段、plist 参数不成对、非法 key 类型和 `%d` 非数字值补充回归测试。 + +### Why +`pyfmt` 被 `(liii logging)` 用于日志消息格式化。当前实现无法区分字段不存在和值为 `#f`,并且缺失字段会丢掉占位符末尾的类型字符,导致输出内容不稳定。 + +### How +1. 将字段查找从“返回字段值”改为“返回匹配到的 pair”,从而区分字段不存在和字段值为 `#f`。 +2. 缺失字段时保留完整原始占位符。 +3. 在 plist 转换阶段显式检查 key/value 是否成对。 + +### 如何测试 +```bash +bin/gf fmt goldfish/liii/string.scm +bin/gf fmt tests/liii/string/pyfmt-test.scm +bin/gf test tests/liii/string/pyfmt-test.scm +bin/gf test tests/liii/string/ +bin/gf test tests/liii/logging-test.scm +``` + ## 2026/05/05 接口设计 ### What diff --git a/goldfish/liii/string.scm b/goldfish/liii/string.scm index 99004ade..5cc5c946 100644 --- a/goldfish/liii/string.scm +++ b/goldfish/liii/string.scm @@ -188,29 +188,28 @@ (define (plist->salist plist) (let loop ((p plist) (result '())) - (if (null? p) - (reverse result) - (let ((key (car p)) (val (cadr p))) - (loop (cddr p) - (cons (cons (cond ((keyword? key) (symbol->string (keyword->symbol key))) - ((symbol? key) (symbol->string key)) - ((string? key) key) - (else (type-error "pyfmt: key must be keyword, symbol or string")) - ) ;cond - val - ) ;cons - result - ) ;cons - ) ;loop - ) ;let - ) ;if + (cond ((null? p) (reverse result)) + ((not (pair? (cdr p))) (type-error "pyfmt: plist requires key-value pairs")) + (else (let ((key (car p)) (val (cadr p))) + (loop (cddr p) + (cons (cons (cond ((keyword? key) (symbol->string (keyword->symbol key))) + ((symbol? key) (symbol->string key)) + ((string? key) key) + (else (type-error "pyfmt: key must be keyword, symbol or string")) + ) ;cond + val + ) ;cons + result + ) ;cons + ) ;loop + ) ;let + ) ;else + ) ;cond ) ;let ) ;define - (define (lookup key alist) - (let ((pair (assoc key alist equal?))) - (and pair (cdr pair)) - ) ;let + (define (lookup-pair key alist) + (assoc key alist equal?) ) ;define (let ((salist (plist->salist plist)) (len (string-length format-string))) @@ -224,9 +223,13 @@ (if (and end-pos (> end-pos (+ pos 2))) (let* ((key (substring format-string (+ pos 2) end-pos)) (type-pos (+ end-pos 1)) - (type-char (if (< type-pos len) (string-ref format-string type-pos) #\s)) - (val (lookup key salist)) - (val-str (cond ((not val) (string-append "%(" key ")")) + (has-type? (< type-pos len)) + (type-char (if has-type? (string-ref format-string type-pos) #\s)) + (placeholder-end (if has-type? (+ type-pos 1) (+ end-pos 1))) + (placeholder (substring format-string pos placeholder-end)) + (pair (lookup-pair key salist)) + (val (and pair (cdr pair))) + (val-str (cond ((not pair) placeholder) ((char=? type-char #\d) (if (number? val) (number->string val) @@ -237,7 +240,9 @@ ) ;cond ) ;val-str ) ; - (loop (+ end-pos 2) (cons val-str (cons (substring format-string i pos) parts))) + (loop placeholder-end + (cons val-str (cons (substring format-string i pos) parts)) + ) ;loop ) ;let* (loop len (cons (substring format-string i len) parts)) ) ;if diff --git a/tests/liii/string/pyfmt-test.scm b/tests/liii/string/pyfmt-test.scm index 9a6b2187..afdc5c4d 100644 --- a/tests/liii/string/pyfmt-test.scm +++ b/tests/liii/string/pyfmt-test.scm @@ -58,4 +58,21 @@ ;; 字段值包含特殊字符 (check (pyfmt "%(path)s" :path "/var/log/app.log") => "/var/log/app.log") +;; 字符串 key +(check (pyfmt "%(name)s" "name" "Bob") => "Bob") + +;; #f 是合法字段值,不能被当作缺失字段 +(check (pyfmt "%(ok)s" :ok #f) => "#f") + +;; 缺失字段时保留完整占位符 +(check (pyfmt "%(name)s") => "%(name)s") +(check (pyfmt "%(age)d") => "%(age)d") +(check (pyfmt "hello %(name)s!" :other "Bob") => "hello %(name)s!") + +;; 参数错误 +(check-catch 'type-error (pyfmt 123)) +(check-catch 'type-error (pyfmt "%(name)s" :name)) +(check-catch 'type-error (pyfmt "%(name)s" 123 "Bob")) +(check-catch 'type-error (pyfmt "%(age)d" :age "30")) + (check-report)