format_code.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. package xlsx
  2. import (
  3. "errors"
  4. "fmt"
  5. "math"
  6. "strconv"
  7. "strings"
  8. )
  9. // Do not edit these attributes once this struct is created. This struct should only be created by
  10. // parseFullNumberFormatString() from a number format string. If the format for a cell needs to change, change
  11. // the number format string and getNumberFormat() will invalidate the old struct and re-parse the string.
  12. type parsedNumberFormat struct {
  13. numFmt string
  14. isTimeFormat bool
  15. negativeFormatExpectsPositive bool
  16. positiveFormat *formatOptions
  17. negativeFormat *formatOptions
  18. zeroFormat *formatOptions
  19. textFormat *formatOptions
  20. parseEncounteredError bool
  21. }
  22. type formatOptions struct {
  23. isTimeFormat bool
  24. showPercent bool
  25. fullFormatString string
  26. reducedFormatString string
  27. prefix string
  28. suffix string
  29. }
  30. func (fullFormat *parsedNumberFormat) FormatValue(cell *Cell) (string, error) {
  31. if cell.cellType != CellTypeNumeric {
  32. textFormat := cell.parsedNumFmt.textFormat
  33. // This switch statement is only for String formats
  34. switch textFormat.reducedFormatString {
  35. case builtInNumFmt[builtInNumFmtIndex_GENERAL]: // General is literally "general"
  36. return cell.Value, nil
  37. case builtInNumFmt[builtInNumFmtIndex_STRING]: // String is "@"
  38. return textFormat.prefix + cell.Value + textFormat.suffix, nil
  39. case "":
  40. return textFormat.prefix + textFormat.suffix, nil
  41. default:
  42. return cell.Value, errors.New("invalid or unsupported format")
  43. }
  44. }
  45. if fullFormat.isTimeFormat {
  46. return fullFormat.parseTime(cell.Value, cell.date1904)
  47. }
  48. var numberFormat *formatOptions
  49. floatVal, floatErr := strconv.ParseFloat(cell.Value, 64)
  50. if floatErr != nil {
  51. return cell.Value, floatErr
  52. }
  53. if floatVal > 0 {
  54. numberFormat = fullFormat.positiveFormat
  55. } else if floatVal < 0 {
  56. if fullFormat.negativeFormatExpectsPositive {
  57. floatVal = math.Abs(floatVal)
  58. }
  59. numberFormat = fullFormat.negativeFormat
  60. } else {
  61. numberFormat = fullFormat.zeroFormat
  62. }
  63. if numberFormat.showPercent {
  64. floatVal = 100 * floatVal
  65. }
  66. // Only the most common format strings are supported here.
  67. // Eventually this switch needs to be replaced with a more general solution.
  68. // Some of these "supported" formats should have thousand separators, but don't get them since Go fmt
  69. // doesn't have a way to request thousands separators.
  70. // The only things that should be supported here are in the array formattingCharacters,
  71. // everything else has been stripped out before.
  72. // The formatting characters can have non-formatting characters mixed in with them and those should be maintained.
  73. // However, at this time we fail to parse those formatting codes and they get replaced with "General"
  74. // This switch statement is only for number formats
  75. var formattedNum string
  76. switch numberFormat.reducedFormatString {
  77. case builtInNumFmt[builtInNumFmtIndex_GENERAL]: // General is literally "general"
  78. // prefix, showPercent, and suffix cannot apply to the general format
  79. // The logic for showing numbers when the format is "general" is much more complicated than the rest of these.
  80. val, err := generalNumericScientific(cell.Value, true)
  81. if err != nil {
  82. return cell.Value, nil
  83. }
  84. return val, nil
  85. case builtInNumFmt[builtInNumFmtIndex_STRING]: // String is "@"
  86. formattedNum = cell.Value
  87. case builtInNumFmt[builtInNumFmtIndex_INT], "#,##0": // Int is "0"
  88. // Previously this case would cast to int and print with %d, but that will not round the value correctly.
  89. formattedNum = fmt.Sprintf("%.0f", floatVal)
  90. case "0.0", "#,##0.0":
  91. formattedNum = fmt.Sprintf("%.1f", floatVal)
  92. case builtInNumFmt[builtInNumFmtIndex_FLOAT], "#,##0.00": // Float is "0.00"
  93. formattedNum = fmt.Sprintf("%.2f", floatVal)
  94. case "0.000", "#,##0.000":
  95. formattedNum = fmt.Sprintf("%.3f", floatVal)
  96. case "0.0000", "#,##0.0000":
  97. formattedNum = fmt.Sprintf("%.4f", floatVal)
  98. case "0.00e+00", "##0.0e+0":
  99. formattedNum = fmt.Sprintf("%e", floatVal)
  100. case "":
  101. // Do nothing.
  102. default:
  103. return cell.Value, nil
  104. }
  105. return numberFormat.prefix + formattedNum + numberFormat.suffix, nil
  106. }
  107. func generalNumericScientific(value string, allowScientific bool) (string, error) {
  108. if strings.TrimSpace(value) == "" {
  109. return "", nil
  110. }
  111. f, err := strconv.ParseFloat(value, 64)
  112. if err != nil {
  113. return value, err
  114. }
  115. if allowScientific {
  116. absF := math.Abs(f)
  117. // When using General format, numbers that are less than 1e-9 (0.000000001) and greater than or equal to
  118. // 1e11 (100,000,000,000) should be shown in scientific notation.
  119. // Numbers less than the number after zero, are assumed to be zero.
  120. if (absF >= math.SmallestNonzeroFloat64 && absF < minNonScientificNumber) || absF >= maxNonScientificNumber {
  121. return strconv.FormatFloat(f, 'E', -1, 64), nil
  122. }
  123. }
  124. // This format (fmt="f", prec=-1) will prevent padding with zeros and will never switch to scientific notation.
  125. // However, it will show more than 11 characters for very precise numbers, and this cannot be changed.
  126. // You could also use fmt="g", prec=11, which doesn't pad with zeros and allows the correct precision,
  127. // but it will use scientific notation on numbers less than 1e-4. That value is hardcoded in Go and cannot be
  128. // configured or disabled.
  129. return strconv.FormatFloat(f, 'f', -1, 64), nil
  130. }
  131. // Format strings are a little strange to compare because empty string needs to be taken as general, and general needs
  132. // to be compared case insensitively.
  133. func compareFormatString(fmt1, fmt2 string) bool {
  134. if fmt1 == fmt2 {
  135. return true
  136. }
  137. if fmt1 == "" || strings.EqualFold(fmt1, "general") {
  138. fmt1 = "general"
  139. }
  140. if fmt2 == "" || strings.EqualFold(fmt2, "general") {
  141. fmt2 = "general"
  142. }
  143. return fmt1 == fmt2
  144. }
  145. func parseFullNumberFormatString(numFmt string) *parsedNumberFormat {
  146. parsedNumFmt := &parsedNumberFormat{
  147. numFmt: numFmt,
  148. }
  149. if isTimeFormat(numFmt) {
  150. // Time formats cannot have multiple groups separated by semicolons, there is only one format.
  151. // Strings are unaffected by the time format.
  152. parsedNumFmt.isTimeFormat = true
  153. parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
  154. return parsedNumFmt
  155. }
  156. var fmtOptions []*formatOptions
  157. formats, err := splitFormatOnSemicolon(numFmt)
  158. if err == nil {
  159. for _, formatSection := range formats {
  160. parsedFormat, err := parseNumberFormatSection(formatSection)
  161. if err != nil {
  162. // If an invalid number section is found, fall back to general
  163. parsedFormat = fallbackErrorFormat
  164. parsedNumFmt.parseEncounteredError = true
  165. }
  166. fmtOptions = append(fmtOptions, parsedFormat)
  167. }
  168. } else {
  169. fmtOptions = append(fmtOptions, fallbackErrorFormat)
  170. parsedNumFmt.parseEncounteredError = true
  171. }
  172. if len(fmtOptions) > 4 {
  173. fmtOptions = []*formatOptions{fallbackErrorFormat}
  174. parsedNumFmt.parseEncounteredError = true
  175. }
  176. if len(fmtOptions) == 1 {
  177. // If there is only one option, it is used for all
  178. parsedNumFmt.positiveFormat = fmtOptions[0]
  179. parsedNumFmt.negativeFormat = fmtOptions[0]
  180. parsedNumFmt.zeroFormat = fmtOptions[0]
  181. if strings.Contains(fmtOptions[0].fullFormatString, "@") {
  182. parsedNumFmt.textFormat = fmtOptions[0]
  183. } else {
  184. parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
  185. }
  186. } else if len(fmtOptions) == 2 {
  187. // If there are two formats, the first is used for positive and zeros, the second gets used as a negative format,
  188. // and strings are not formatted.
  189. // When negative numbers now have their own format, they should become positive before having the format applied.
  190. // The format will contain a negative sign if it is desired, but they may be colored red or wrapped in
  191. // parenthesis instead.
  192. parsedNumFmt.negativeFormatExpectsPositive = true
  193. parsedNumFmt.positiveFormat = fmtOptions[0]
  194. parsedNumFmt.negativeFormat = fmtOptions[1]
  195. parsedNumFmt.zeroFormat = fmtOptions[0]
  196. parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
  197. } else if len(fmtOptions) == 3 {
  198. // If there are three formats, the first is used for positive, the second gets used as a negative format,
  199. // the third is for negative, and strings are not formatted.
  200. parsedNumFmt.negativeFormatExpectsPositive = true
  201. parsedNumFmt.positiveFormat = fmtOptions[0]
  202. parsedNumFmt.negativeFormat = fmtOptions[1]
  203. parsedNumFmt.zeroFormat = fmtOptions[2]
  204. parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
  205. } else {
  206. // With four options, the first is positive, the second is negative, the third is zero, and the fourth is strings
  207. // Negative numbers should be still become positive before having the negative formatting applied.
  208. parsedNumFmt.negativeFormatExpectsPositive = true
  209. parsedNumFmt.positiveFormat = fmtOptions[0]
  210. parsedNumFmt.negativeFormat = fmtOptions[1]
  211. parsedNumFmt.zeroFormat = fmtOptions[2]
  212. parsedNumFmt.textFormat = fmtOptions[3]
  213. }
  214. return parsedNumFmt
  215. }
  216. // splitFormatOnSemicolon will split the format string into the format sections
  217. // This logic to split the different formats on semicolon is fully correct, and will skip all literal semicolons,
  218. // and will catch all breaking semicolons.
  219. func splitFormatOnSemicolon(format string) ([]string, error) {
  220. var formats []string
  221. prevIndex := 0
  222. for i := 0; i < len(format); i++ {
  223. if format[i] == ';' {
  224. formats = append(formats, format[prevIndex:i])
  225. prevIndex = i + 1
  226. } else if format[i] == '\\' {
  227. i++
  228. } else if format[i] == '"' {
  229. endQuoteIndex := strings.Index(format[i+1:], "\"")
  230. if endQuoteIndex == -1 {
  231. // This is an invalid format string, fall back to general
  232. return nil, errors.New("invalid format string")
  233. }
  234. i += endQuoteIndex + 1
  235. }
  236. }
  237. return append(formats, format[prevIndex:]), nil
  238. }
  239. var fallbackErrorFormat = &formatOptions{
  240. fullFormatString: "general",
  241. reducedFormatString: "general",
  242. }
  243. // parseNumberFormatSection takes in individual format and parses out most of the options.
  244. // Some options are parsed, removed from the string, and set as settings on formatOptions.
  245. // There remainder of the format string is put in the reducedFormatString attribute, and supported values for these
  246. // are handled in a switch in the Cell.FormattedValue() function.
  247. // Ideally more and more of the format string would be parsed out here into settings until there is no remainder string
  248. // at all.
  249. // Features that this supports:
  250. // - Time formats are detected, and marked in the options. Time format strings are handled when doing the formatting.
  251. // The logic to detect time formats is currently not correct, and can catch formats that are not time formats as well
  252. // as miss formats that are time formats.
  253. // - Color formats are detected and removed.
  254. // - Currency annotations are handled properly.
  255. // - Literal strings wrapped in quotes are handled and put into prefix or suffix.
  256. // - Numbers that should be percent are detected and marked in the options.
  257. // - Conditionals are detected and removed, but they are not obeyed. The conditional groups will be used just like the
  258. // positive;negative;zero;string format groups. Here is an example of a conditional format: "[Red][<=100];[Blue][>100]"
  259. // Decoding the actual number formatting portion is out of scope, that is placed into reducedFormatString and is used
  260. // when formatting the string. The string there will be reduced to only the things in the formattingCharacters array.
  261. // Everything not in that array has been parsed out and put into formatOptions.
  262. func parseNumberFormatSection(fullFormat string) (*formatOptions, error) {
  263. reducedFormat := strings.TrimSpace(fullFormat)
  264. // general is the only format that does not use the normal format symbols notations
  265. if compareFormatString(reducedFormat, "general") {
  266. return &formatOptions{
  267. fullFormatString: "general",
  268. reducedFormatString: "general",
  269. }, nil
  270. }
  271. prefix, reducedFormat, showPercent1, err := parseLiterals(reducedFormat)
  272. if err != nil {
  273. return nil, err
  274. }
  275. reducedFormat, suffixFormat := splitFormatAndSuffixFormat(reducedFormat)
  276. suffix, remaining, showPercent2, err := parseLiterals(suffixFormat)
  277. if err != nil {
  278. return nil, err
  279. }
  280. if len(remaining) > 0 {
  281. // This paradigm of codes consisting of literals, number formats, then more literals is not always correct, they can
  282. // actually be intertwined. Though 99% of the time number formats will not do this.
  283. // Excel uses this format string for Social Security Numbers: 000\-00\-0000
  284. // and this for US phone numbers: [<=9999999]###\-####;\(###\)\ ###\-####
  285. return nil, errors.New("invalid or unsupported format string")
  286. }
  287. return &formatOptions{
  288. fullFormatString: fullFormat,
  289. isTimeFormat: false,
  290. reducedFormatString: reducedFormat,
  291. prefix: prefix,
  292. suffix: suffix,
  293. showPercent: showPercent1 || showPercent2,
  294. }, nil
  295. }
  296. // formattingCharacters will be left in the reducedNumberFormat
  297. // It is important that these be looked for in order so that the slash cases are handled correctly.
  298. // / (slash) is a fraction format if preceded by 0, #, or ?, otherwise it is not a formatting character
  299. // E- E+ e- e+ are scientific notation, but E, e, -, + are not formatting characters independently
  300. // \ (back slash) makes the next character a literal (not formatting)
  301. // " Anything in double quotes is not a formatting character
  302. // _ (underscore) skips the width of the next character, so the next character cannot be formatting
  303. var formattingCharacters = []string{"0/", "#/", "?/", "E-", "E+", "e-", "e+", "0", "#", "?", ".", ",", "@", "*"}
  304. // The following are also time format characters, but since this is only used for detecting, not decoding, they are
  305. // redundant here: ee, gg, ggg, rr, ss, mm, hh, yyyy, dd, ddd, dddd, mm, mmm, mmmm, mmmmm, ss.0000, ss.000, ss.00, ss.0
  306. // The .00 type format is very tricky, because it only counts if it comes after ss or s or [ss] or [s]
  307. // .00 is actually a valid number format by itself.
  308. var timeFormatCharacters = []string{"m", "d", "yy", "h", "m", "AM/PM", "A/P", "am/pm", "a/p", "r", "g", "e", "b1", "b2", "[hh]", "[h]", "[mm]", "[m]",
  309. "s.0000", "s.000", "s.00", "s.0", "s", "[ss].0000", "[ss].000", "[ss].00", "[ss].0", "[ss]", "[s].0000", "[s].000", "[s].00", "[s].0", "[s]"}
  310. func splitFormatAndSuffixFormat(format string) (string, string) {
  311. var i int
  312. for ; i < len(format); i++ {
  313. curReducedFormat := format[i:]
  314. var found bool
  315. for _, special := range formattingCharacters {
  316. if strings.HasPrefix(curReducedFormat, special) {
  317. // Skip ahead if the special character was longer than length 1
  318. i += len(special) - 1
  319. found = true
  320. break
  321. }
  322. }
  323. if !found {
  324. break
  325. }
  326. }
  327. suffixFormat := format[i:]
  328. format = format[:i]
  329. return format, suffixFormat
  330. }
  331. func parseLiterals(format string) (string, string, bool, error) {
  332. var prefix string
  333. showPercent := false
  334. for i := 0; i < len(format); i++ {
  335. curReducedFormat := format[i:]
  336. switch curReducedFormat[0] {
  337. case '\\':
  338. // If there is a slash, skip the next character, and add it to the prefix
  339. if len(curReducedFormat) > 1 {
  340. i++
  341. prefix += curReducedFormat[1:2]
  342. }
  343. case '_':
  344. // If there is an underscore, skip the next character, but don't add it to the prefix
  345. if len(curReducedFormat) > 1 {
  346. i++
  347. }
  348. case '*':
  349. // Asterisks are used to repeat the next character to fill the full cell width.
  350. // There isn't really a cell size in this context, so this will be ignored.
  351. case '"':
  352. // If there is a quote skip to the next quote, and add the quoted characters to the prefix
  353. endQuoteIndex := strings.Index(curReducedFormat[1:], "\"")
  354. if endQuoteIndex == -1 {
  355. return "", "", false, errors.New("invalid formatting code")
  356. }
  357. prefix = prefix + curReducedFormat[1:endQuoteIndex+1]
  358. i += endQuoteIndex + 1
  359. case '%':
  360. showPercent = true
  361. prefix += "%"
  362. case '[':
  363. // Brackets can be currency annotations (e.g. [$$-409])
  364. // color formats (e.g. [color1] through [color56], as well as [red] etc.)
  365. // conditionals (e.g. [>100], the valid conditionals are =, >, <, >=, <=, <>)
  366. bracketIndex := strings.Index(curReducedFormat, "]")
  367. if bracketIndex == -1 {
  368. return "", "", false, errors.New("invalid formatting code")
  369. }
  370. // Currencies in Excel are annotated with this format: [$<Currency String>-<Language Info>]
  371. // Currency String is something like $, ¥, €, or £
  372. // Language Info is three hexadecimal characters
  373. if len(curReducedFormat) > 2 && curReducedFormat[1] == '$' {
  374. dashIndex := strings.Index(curReducedFormat, "-")
  375. if dashIndex != -1 && dashIndex < bracketIndex {
  376. // Get the currency symbol, and skip to the end of the currency format
  377. prefix += curReducedFormat[2:dashIndex]
  378. } else {
  379. return "", "", false, errors.New("invalid formatting code")
  380. }
  381. }
  382. i += bracketIndex
  383. case '$', '-', '+', '/', '(', ')', ':', '!', '^', '&', '\'', '~', '{', '}', '<', '>', '=', ' ':
  384. // These symbols are allowed to be used as literal without escaping
  385. prefix += curReducedFormat[0:1]
  386. default:
  387. for _, special := range formattingCharacters {
  388. if strings.HasPrefix(curReducedFormat, special) {
  389. // This means we found the start of the actual number formatting portion, and should return.
  390. return prefix, format[i:], showPercent, nil
  391. }
  392. }
  393. // Symbols that don't have meaning and aren't in the exempt literal characters, but be escaped.
  394. return "", "", false, errors.New("invalid formatting code")
  395. }
  396. }
  397. return prefix, "", showPercent, nil
  398. }
  399. // parseTime returns a string parsed using time.Time
  400. func (fullFormat *parsedNumberFormat) parseTime(value string, date1904 bool) (string, error) {
  401. f, err := strconv.ParseFloat(value, 64)
  402. if err != nil {
  403. return value, err
  404. }
  405. val := TimeFromExcelTime(f, date1904)
  406. format := fullFormat.numFmt
  407. // Replace Excel placeholders with Go time placeholders.
  408. // For example, replace yyyy with 2006. These are in a specific order,
  409. // due to the fact that m is used in month, minute, and am/pm. It would
  410. // be easier to fix that with regular expressions, but if it's possible
  411. // to keep this simple it would be easier to maintain.
  412. // Full-length month and days (e.g. March, Tuesday) have letters in them that would be replaced
  413. // by other characters below (such as the 'h' in March, or the 'd' in Tuesday) below.
  414. // First we convert them to arbitrary characters unused in Excel Date formats, and then at the end,
  415. // turn them to what they should actually be.
  416. // Based off: http://www.ozgrid.com/Excel/CustomFormats.htm
  417. replacements := []struct{ xltime, gotime string }{
  418. {"yyyy", "2006"},
  419. {"yy", "06"},
  420. {"mmmm", "%%%%"},
  421. {"dddd", "&&&&"},
  422. {"dd", "02"},
  423. {"d", "2"},
  424. {"mmm", "Jan"},
  425. {"mmss", "0405"},
  426. {"ss", "05"},
  427. {"mm:", "04:"},
  428. {":mm", ":04"},
  429. {"mm", "01"},
  430. {"am/pm", "pm"},
  431. {"m/", "1/"},
  432. {"%%%%", "January"},
  433. {"&&&&", "Monday"},
  434. }
  435. // It is the presence of the "am/pm" indicator that determins
  436. // if this is a 12 hour or 24 hours time format, not the
  437. // number of 'h' characters.
  438. if is12HourTime(format) {
  439. format = strings.Replace(format, "hh", "03", 1)
  440. format = strings.Replace(format, "h", "3", 1)
  441. } else {
  442. format = strings.Replace(format, "hh", "15", 1)
  443. format = strings.Replace(format, "h", "15", 1)
  444. }
  445. for _, repl := range replacements {
  446. format = strings.Replace(format, repl.xltime, repl.gotime, 1)
  447. }
  448. // If the hour is optional, strip it out, along with the
  449. // possible dangling colon that would remain.
  450. if val.Hour() < 1 {
  451. format = strings.Replace(format, "]:", "]", 1)
  452. format = strings.Replace(format, "[03]", "", 1)
  453. format = strings.Replace(format, "[3]", "", 1)
  454. format = strings.Replace(format, "[15]", "", 1)
  455. } else {
  456. format = strings.Replace(format, "[3]", "3", 1)
  457. format = strings.Replace(format, "[15]", "15", 1)
  458. }
  459. return val.Format(format), nil
  460. }
  461. // isTimeFormat checks whether an Excel format string represents a time.Time.
  462. // This function is now correct, but it can detect time format strings that cannot be correctly handled by parseTime()
  463. func isTimeFormat(format string) bool {
  464. var foundTimeFormatCharacters bool
  465. for i := 0; i < len(format); i++ {
  466. curReducedFormat := format[i:]
  467. switch curReducedFormat[0] {
  468. case '\\', '_':
  469. // If there is a slash, skip the next character, and add it to the prefix
  470. // If there is an underscore, skip the next character, but don't add it to the prefix
  471. if len(curReducedFormat) > 1 {
  472. i++
  473. }
  474. case '*':
  475. // Asterisks are used to repeat the next character to fill the full cell width.
  476. // There isn't really a cell size in this context, so this will be ignored.
  477. case '"':
  478. // If there is a quote skip to the next quote, and add the quoted characters to the prefix
  479. endQuoteIndex := strings.Index(curReducedFormat[1:], "\"")
  480. if endQuoteIndex == -1 {
  481. // This is not any type of valid format.
  482. return false
  483. }
  484. i += endQuoteIndex + 1
  485. case '$', '-', '+', '/', '(', ')', ':', '!', '^', '&', '\'', '~', '{', '}', '<', '>', '=', ' ':
  486. // These symbols are allowed to be used as literal without escaping
  487. case ',':
  488. // This is not documented in the XLSX spec as far as I can tell, but Excel and Numbers will include
  489. // commas in number formats without escaping them, so this should be supported.
  490. default:
  491. foundInThisLoop := false
  492. for _, special := range timeFormatCharacters {
  493. if strings.HasPrefix(curReducedFormat, special) {
  494. foundTimeFormatCharacters = true
  495. foundInThisLoop = true
  496. i += len(special) - 1
  497. break
  498. }
  499. }
  500. if foundInThisLoop {
  501. continue
  502. }
  503. if curReducedFormat[0] == '[' {
  504. // For number formats, this code would happen above in a case '[': section.
  505. // However, for time formats it must happen after looking for occurrences in timeFormatCharacters
  506. // because there are a few time formats that can be wrapped in brackets.
  507. // Brackets can be currency annotations (e.g. [$$-409])
  508. // color formats (e.g. [color1] through [color56], as well as [red] etc.)
  509. // conditionals (e.g. [>100], the valid conditionals are =, >, <, >=, <=, <>)
  510. bracketIndex := strings.Index(curReducedFormat, "]")
  511. if bracketIndex == -1 {
  512. // This is not any type of valid format.
  513. return false
  514. }
  515. i += bracketIndex
  516. continue
  517. }
  518. // Symbols that don't have meaning, aren't in the exempt literal characters, and aren't escaped are invalid.
  519. // The string could still be a valid number format string.
  520. return false
  521. }
  522. }
  523. // If the string doesn't have any time formatting characters, it could technically be a time format, but it
  524. // would be a pretty weak time format. A valid time format with no time formatting symbols will also be a number
  525. // format with no number formatting symbols, which is essentially a constant string that does not depend on the
  526. // cell's value in anyway. The downstream logic will do the right thing in that case if this returns false.
  527. return foundTimeFormatCharacters
  528. }
  529. // is12HourTime checks whether an Excel time format string is a 12
  530. // hours form.
  531. func is12HourTime(format string) bool {
  532. return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
  533. }