// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Transform function — PICTURE format processing for @ SAY and GET. // Harbour: src/rtl/transfrm.c // // Picture string format: // @B Left-align numeric // @C Append " CR" for positive numbers // @D Date format // @E European format (swap . and ,) // @( Parentheses for negative // @R Non-template chars in mask are inserted (not stored) // @! Convert to uppercase // @Z Display blank for zero // @S Scroll width // @L Pad with leading zeros // // Mask chars: 9 # ! A N X Y L . , package hbrtl import ( "five/hbrt" "fmt" "strings" ) // transformHbValue applies a PICTURE format to an hbrt.Value. func transformHbValue(v hbrt.Value, pic string) string { if pic == "" { return valueToDisplay(v) } funcFlags, mask := parsePicture(pic) if v.IsString() { return transformString(v.AsString(), funcFlags, mask) } if v.IsNumeric() { if v.IsNumInt() { return transformNumeric(float64(v.AsNumInt()), funcFlags, mask) } return transformNumeric(v.AsNumDouble(), funcFlags, mask) } if v.IsLogical() { if v.AsBool() { return "T" } return "F" } return valueToDisplay(v) } // parsePicture splits "@flags mask" into flags and mask. func parsePicture(pic string) (string, string) { if len(pic) == 0 || pic[0] != '@' { return "", pic } // Find space separating function from mask idx := strings.IndexByte(pic, ' ') if idx < 0 { return strings.ToUpper(pic[1:]), "" } return strings.ToUpper(pic[1:idx]), pic[idx+1:] } func transformString(s string, flags string, mask string) string { upper := strings.ContainsRune(flags, '!') if mask == "" { if upper { return strings.ToUpper(s) } return s } // Apply mask var out strings.Builder si := 0 for _, m := range mask { if si >= len(s) { break } ch := s[si] switch m { case '!': if ch >= 'a' && ch <= 'z' { ch = ch - 32 } out.WriteByte(ch) si++ case 'A', 'N', 'X', '9', '#': if upper && ch >= 'a' && ch <= 'z' { ch = ch - 32 } out.WriteByte(ch) si++ default: // Non-template char: if @R, insert literal; else consume if strings.ContainsRune(flags, 'R') { out.WriteRune(m) } else { out.WriteByte(ch) si++ } } } return out.String() } func transformNumeric(n float64, flags string, mask string) string { blankZero := strings.ContainsRune(flags, 'Z') leftAlign := strings.ContainsRune(flags, 'B') credit := strings.ContainsRune(flags, 'C') debit := strings.ContainsRune(flags, 'X') paren := strings.ContainsRune(flags, '(') padZero := strings.ContainsRune(flags, 'L') if blankZero && n == 0 { if mask != "" { return strings.Repeat(" ", len(mask)) } return " " } if mask == "" { s := fmt.Sprintf("%g", n) if leftAlign { s = strings.TrimLeft(s, " ") } return s } // Count digit positions and decimal nDec := 0 nInt := 0 decIdx := strings.IndexByte(mask, '.') for _, m := range mask { if m == '9' || m == '#' { if decIdx >= 0 && nInt+nDec >= decIdx { nDec++ } else { nInt++ } } } // Format the number var formatted string if nDec > 0 { formatted = fmt.Sprintf("%*.*f", len(mask), nDec, n) } else { formatted = fmt.Sprintf("%*.0f", len(mask), n) } // Pad with zeros if @L if padZero { neg := n < 0 stripped := strings.TrimLeft(formatted, " ") if neg { stripped = strings.TrimLeft(stripped, "-") formatted = "-" + strings.Repeat("0", len(mask)-len(stripped)-1) + stripped } else { formatted = strings.Repeat("0", len(mask)-len(stripped)) + stripped } } // Apply mask character by character var out strings.Builder fi := 0 if len(formatted) > len(mask) { fi = len(formatted) - len(mask) } for _, m := range mask { if fi >= len(formatted) { out.WriteRune(m) continue } switch m { case '9', '#': out.WriteByte(formatted[fi]) fi++ case '.': out.WriteByte('.') fi++ case ',': if fi < len(formatted) && formatted[fi] == ',' { out.WriteByte(',') fi++ } else if fi < len(formatted) && (formatted[fi] >= '0' && formatted[fi] <= '9') { out.WriteByte(',') } else { out.WriteByte(' ') fi++ } default: out.WriteRune(m) fi++ } } result := out.String() if leftAlign { result = strings.TrimLeft(result, " ") result = result + strings.Repeat(" ", len(mask)-len(result)) } // Parentheses for negative if paren && n < 0 { result = strings.Replace(result, "-", "(", 1) result = strings.TrimRight(result, " ") + ")" } // Credit/Debit suffix if credit && n >= 0 { result += " CR" } if debit && n < 0 { result += " DB" } return result }