oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package format
2
3import "strings"
4
5// sectionMode classifies one ';'-separated format section so renderSection
6// knows which tokenizer to run.
7type sectionMode int
8
9const (
10 modeLiteral sectionMode = iota // no placeholders: always the same text
11 modeNumeric
12 modeDate
13)
14
15// detectMode scans a section for bare (unquoted, unescaped) characters
16// that decide its kind: any of yYmMdDhHsS means a date/time section, else
17// any of 0# means a numeric section, else the section is pure literal
18// text. This must run before tokenizing, because a date section and a
19// numeric section use different rules for the same bytes (',' is a
20// thousands separator in a numeric section, a literal in a date one).
21func detectMode(section string) sectionMode {
22 sawDate := false
23 sawNumeric := false
24 walkBareRunes(section, func(r byte) bool {
25 switch {
26 case strings.IndexByte("yYmMdDhHsS", r) >= 0:
27 sawDate = true
28 return true // date wins outright — stop scanning early
29 case r == '0' || r == '#':
30 sawNumeric = true
31 }
32 return false
33 })
34 if sawDate {
35 return modeDate
36 }
37 if sawNumeric {
38 return modeNumeric
39 }
40 return modeLiteral
41}
42
43// walkBareRunes calls fn for every byte in section NOT inside a "quoted"
44// literal and not the escaped character following a backslash. Stops
45// early if fn returns true.
46func walkBareRunes(section string, fn func(byte) bool) {
47 inQuote := false
48 for i := 0; i < len(section); i++ {
49 c := section[i]
50 switch {
51 case c == '\\' && i+1 < len(section):
52 i++ // skip the escaped character itself
53 case c == '"':
54 inQuote = !inQuote
55 case inQuote:
56 // inside a literal: not a bare rune
57 default:
58 if fn(c) {
59 return
60 }
61 }
62 }
63}
64
65// rendersNumber reports whether section actually renders the number
66// (via "General", numeric, or date placeholders) rather than being a
67// fixed literal label — used to decide whether a single-section format
68// gets an automatic "-" for a negative value.
69func rendersNumber(section string) bool {
70 if strings.EqualFold(strings.TrimSpace(section), "General") {
71 return true
72 }
73 return detectMode(section) != modeLiteral
74}
75
76// renderSection renders val (already sign-adjusted by the caller)
77// through one format section. ok is false when the section uses a
78// construct this package does not support, telling the caller to fall
79// back to General.
80func renderSection(section string, val float64) (string, bool) {
81 if strings.EqualFold(strings.TrimSpace(section), "General") {
82 return generalNumber(val), true
83 }
84 switch detectMode(section) {
85 case modeDate:
86 return renderDate(section, val)
87 case modeNumeric:
88 return renderNumeric(section, val)
89 default:
90 return renderLiteral(section)
91 }
92}
93
94// renderLiteral renders a section with no digit or date placeholders at
95// all: the same text regardless of val (e.g. a section used purely to
96// hide a value, or to show a fixed label like "-" or "n/a"). A bare
97// ASCII letter is still rejected — Excel reserves unquoted letters for
98// format tokens, so an unrecognized one (e.g. a stray "q") is treated
99// as an unsupported construct rather than assumed to be a literal word.
100func renderLiteral(section string) (string, bool) {
101 var sb strings.Builder
102 ok := true
103 walkLiteralAndPlaceholders(section, func(lit string) { sb.WriteString(lit) }, func(c byte) bool {
104 if isASCIILetter(c) || strings.IndexByte("?@_*", c) >= 0 {
105 ok = false
106 return true
107 }
108 sb.WriteByte(c)
109 return false
110 })
111 return sb.String(), ok
112}
113
114// renderTextSection renders the 4th (text) section: '@' inserts s
115// verbatim, everything else is literal.
116func renderTextSection(section, s string) (string, bool) {
117 var sb strings.Builder
118 ok := true
119 walkLiteralAndPlaceholders(section, func(lit string) { sb.WriteString(lit) }, func(c byte) bool {
120 if c == '@' {
121 sb.WriteString(s)
122 return false
123 }
124 ok = false
125 return true
126 })
127 return sb.String(), ok
128}
129
130// walkLiteralAndPlaceholders walks section, resolving quoted "..." runs
131// and \-escaped characters into literal text passed to onLiteral, and
132// passing every other bare byte to onOther. onOther returns true to
133// abort (unsupported construct).
134func walkLiteralAndPlaceholders(section string, onLiteral func(string), onOther func(byte) bool) {
135 i := 0
136 for i < len(section) {
137 c := section[i]
138 switch {
139 case c == '\\' && i+1 < len(section):
140 onLiteral(string(section[i+1]))
141 i += 2
142 case c == '"':
143 j := strings.IndexByte(section[i+1:], '"')
144 if j < 0 {
145 onLiteral(section[i+1:])
146 i = len(section)
147 } else {
148 onLiteral(section[i+1 : i+1+j])
149 i = i + 1 + j + 1
150 }
151 default:
152 if onOther(c) {
153 return
154 }
155 i++
156 }
157 }
158}
159