Files
five/hbrtl/charset.go
CharlesKWON d5e15272d2 feat(charset): UTF-8 default string semantics with selectable charset
Five strings now operate in Unicode rune units by default. Core string
functions (LEN/CHR/ASC/SUBSTR/LEFT/RIGHT/AT/PADR/PADL) are charset-aware:
UTF-8 rune semantics by default, byte/charset semantics when a legacy
charset (CP949, CP1252, ...) is selected. Initial charset is settable via
FIVE_CHARSET / HB_CODEPAGE env vars; default UTF8.

- hbrtl/charset.go: charset state + Str* helpers + DecodeToUTF8/EncodeFromUTF8
  + RTL HB_GETCHARSET/HB_SETCHARSET/HB_CDPSELECT/HB_TRANSLATE (x/text htmlindex)
- compiler/gengo: inlined string intrinsics now call charset-aware hbrtl.Str*
  helpers instead of byte-based Go (they previously bypassed the RTL registry)
- compiler/analyzer: register HB_GETCHARSET/HB_SETCHARSET/HB_TRANSLATE as known
- hbrtl/regex.go: add HB_REGEX (array-of-submatches)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:42:33 +09:00

320 lines
7.3 KiB
Go

// charset.go — Five 문자셋(charset/codepage) 서브시스템.
//
// 설계
// - Five 의 문자열 기본 인코딩은 UTF-8 이다(Go 네이티브). 코어 문자열
// 함수(CHR/ASC/LEN/SUBSTR/LEFT/RIGHT/AT)는 활성 charset 이 UTF-8 이면
// '문자(rune)' 단위로 동작한다.
// - 활성 charset 을 지정하면(예: CP949, CP1252) 그 charset 의 의미로
// 동작하고, 입출력 경계 변환은 HB_TRANSLATE / 디코드·인코드 헬퍼로 한다.
// - 정의하지 않으면 UTF-8. 초기값은 환경변수 FIVE_CHARSET(또는 HB_CODEPAGE)
// 로 지정할 수 있다.
//
// PRG surface
// HB_GETCHARSET() → cCurrent (예: "UTF8")
// HB_SETCHARSET([cName]) → cPrev (인자 없으면 조회만)
// HB_CDPSELECT([cName]) → cPrev (Harbour 호환 별칭)
// HB_TRANSLATE(cStr, cFrom, cTo) → cConverted (charset 간 변환)
package hbrtl
import (
"os"
"strings"
"sync"
"unicode/utf8"
"five/hbrt"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
)
var (
csMu sync.RWMutex
csName = "UTF8"
)
func init() {
v := os.Getenv("FIVE_CHARSET")
if v == "" {
v = os.Getenv("HB_CODEPAGE")
}
if v != "" {
csName = normCharset(v)
}
}
// normCharset — 표기 정규화(대문자, 하이픈 제거 일부). "utf-8" → "UTF8".
func normCharset(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
switch s {
case "UTF-8", "UTF8", "":
return "UTF8"
}
return s
}
// charsetIsUTF8 — 활성 charset 이 UTF-8 인가(기본값).
func charsetIsUTF8() bool { return csIsUTF8(GetCharset()) }
func csIsUTF8(name string) bool {
switch normCharset(name) {
case "UTF8", "UTF-8", "":
return true
}
return false
}
// GetCharset / SetCharset — 활성 charset 조회/설정. SetCharset 은 이전값 반환.
func GetCharset() string {
csMu.RLock()
defer csMu.RUnlock()
return csName
}
func SetCharset(name string) string {
csMu.Lock()
defer csMu.Unlock()
prev := csName
if strings.TrimSpace(name) != "" {
csName = normCharset(name)
}
return prev
}
// encodingFor — charset 이름으로 x/text encoding 획득. UTF-8/미지원이면 nil.
func encodingFor(name string) encoding.Encoding {
if csIsUTF8(name) {
return nil // identity
}
// htmlindex 는 "euc-kr","windows-1252","shift_jis" 등 표준 별칭을 받는다.
alias := strings.ToLower(strings.TrimSpace(name))
switch normCharset(name) {
case "CP949", "MS949", "EUCKR", "EUC-KR", "KSC5601":
alias = "euc-kr"
case "CP1252", "WINDOWS1252", "WINDOWS-1252", "LATIN1", "ISO8859-1", "ISO-8859-1":
alias = "windows-1252"
case "CP932", "SHIFTJIS", "SJIS", "SHIFT-JIS":
alias = "shift_jis"
case "CP936", "GBK", "GB2312":
alias = "gbk"
case "CP950", "BIG5":
alias = "big5"
}
if enc, err := htmlindex.Get(alias); err == nil {
return enc
}
return nil
}
// DecodeToUTF8 — 지정 charset 바이트열을 내부 UTF-8 문자열로 디코드.
func DecodeToUTF8(b []byte, fromCharset string) string {
enc := encodingFor(fromCharset)
if enc == nil {
return string(b)
}
if out, err := enc.NewDecoder().Bytes(b); err == nil {
return string(out)
}
return string(b)
}
// EncodeFromUTF8 — 내부 UTF-8 문자열을 지정 charset 바이트열로 인코드.
func EncodeFromUTF8(s, toCharset string) []byte {
enc := encodingFor(toCharset)
if enc == nil {
return []byte(s)
}
if out, err := enc.NewEncoder().Bytes([]byte(s)); err == nil {
return out
}
return []byte(s)
}
// ── charset-aware 코어 문자열 헬퍼 ───────────────────────────────────────
// 컴파일러(gengo)가 LEN/CHR/ASC/SUBSTR/LEFT/RIGHT/AT 를 인라인으로 펼칠 때
// 이 헬퍼들을 호출한다. 활성 charset 이 UTF-8(기본)이면 rune(문자) 단위,
// 아니면 byte 단위로 동작한다.
// StrLen — LEN(cString): charset 단위 길이.
func StrLen(s string) int {
if charsetIsUTF8() {
return utf8.RuneCountInString(s)
}
return len(s)
}
// StrChr — CHR(nCode): charset 단위 1문자 생성.
func StrChr(n int) string {
if charsetIsUTF8() {
if n < 0 {
n = 0
}
return string(rune(n))
}
return string([]byte{byte(n)})
}
// StrAsc — ASC(cString): 첫 문자의 코드값.
func StrAsc(s string) int {
if s == "" {
return 0
}
if charsetIsUTF8() {
r, _ := utf8.DecodeRuneInString(s)
return int(r)
}
return int(s[0])
}
// StrSubStr — SUBSTR(cString, nStart, nLen): nStart 는 1-기반.
// hasLen 이 false 면 nStart 부터 끝까지.
func StrSubStr(s string, start, length int, hasLen bool) string {
if charsetIsUTF8() {
rs := []rune(s)
n := len(rs)
sp := start - 1
if sp < 0 {
sp = 0
}
if sp > n {
sp = n
}
sl := length
if !hasLen {
sl = n - sp
}
if sl < 0 {
sl = 0
}
if sp+sl > n {
sl = n - sp
}
return string(rs[sp : sp+sl])
}
n := len(s)
sp := start - 1
if sp < 0 {
sp = 0
}
if sp > n {
sp = n
}
sl := length
if !hasLen {
sl = n - sp
}
if sl < 0 {
sl = 0
}
if sp+sl > n {
sl = n - sp
}
return s[sp : sp+sl]
}
// StrLeft — LEFT(cString, nLen).
func StrLeft(s string, n int) string {
if n <= 0 {
return ""
}
if charsetIsUTF8() {
rs := []rune(s)
if n >= len(rs) {
return s
}
return string(rs[:n])
}
if n >= len(s) {
return s
}
return s[:n]
}
// StrRight — RIGHT(cString, nLen).
func StrRight(s string, n int) string {
if n <= 0 {
return ""
}
if charsetIsUTF8() {
rs := []rune(s)
if n >= len(rs) {
return s
}
return string(rs[len(rs)-n:])
}
if n >= len(s) {
return s
}
return s[len(s)-n:]
}
// StrAt — AT(cSearch, cTarget): cTarget 안에서 cSearch 의 1-기반 위치, 없으면 0.
func StrAt(search, target string) int {
idx := strings.Index(target, search)
if idx < 0 {
return 0
}
if charsetIsUTF8() {
return utf8.RuneCountInString(target[:idx]) + 1
}
return idx + 1
}
// StrPadR — PADR(cString, nLen): 오른쪽 공백 패딩(초과 시 왼쪽 nLen 컷).
func StrPadR(s string, n int) string {
l := StrLen(s)
if l >= n {
return StrLeft(s, n)
}
return s + Spaces(n-l)
}
// StrPadL — PADL(cString, nLen [, cFill]): 왼쪽 패딩(초과 시 오른쪽 nLen 컷).
func StrPadL(s string, n int, fill string) string {
l := StrLen(s)
if l >= n {
return StrRight(s, n)
}
fc := " "
if fill != "" {
if charsetIsUTF8() {
fc = string([]rune(fill)[:1])
} else {
fc = fill[:1]
}
}
return strings.Repeat(fc, n-l) + s
}
// ── PRG RTL ──────────────────────────────────────────────────────────
// HbGetCharset: HB_GETCHARSET() → cName
func HbGetCharset(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProcFast()
t.PushString(GetCharset())
t.RetValue()
}
// HbSetCharset: HB_SETCHARSET([cName]) → cPrev
func HbSetCharset(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProcFast()
name := ""
if nParams >= 1 {
name = t.Local(1).AsString()
}
t.PushString(SetCharset(name))
t.RetValue()
}
// HbTranslate: HB_TRANSLATE(cStr, cFrom, cTo) → cConverted
func HbTranslate(t *hbrt.Thread) {
t.Frame(3, 0)
defer t.EndProcFast()
s := t.Local(1).AsString()
from := t.Local(2).AsString()
to := t.Local(3).AsString()
t.PushString(string(EncodeFromUTF8(DecodeToUTF8([]byte(s), from), to)))
t.RetValue()
}