Skip to content
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
24 changes: 24 additions & 0 deletions devel/0022.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 29 additions & 24 deletions goldfish/liii/string.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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)
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions tests/liii/string/pyfmt-test.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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)