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>
320 lines
7.3 KiB
Go
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()
|
|
}
|