aboutsummaryrefslogtreecommitdiff
path: root/internal/render/ascii.go
blob: 077a4186d7ae2a7721256458d6d341b2729b3ad0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package render

import "strings"

// asciiMap is deliberately one rune to one ASCII character.
//
// Substitution happens after the table has been laid out, so anything that
// changed the number of characters would shear every column to its right. That
// constraint is why the wind arrows become single letters rather than the
// two-letter compass points that would read better.
var asciiMap = map[rune]string{
	// Polish diacritics, so a Polish forecast survives GSM-7.
	'ą': "a", 'ć': "c", 'ę': "e", 'ł': "l", 'ń': "n",
	'ó': "o", 'ś': "s", 'ź': "z", 'ż': "z",
	'Ą': "A", 'Ć': "C", 'Ę': "E", 'Ł': "L", 'Ń': "N",
	'Ó': "O", 'Ś': "S", 'Ź': "Z", 'Ż': "Z",

	// Wind arrows. The arrow points the way the wind blows, so v is a northerly.
	'↑': "^", '↓': "v", '←': "<", '→': ">",
	'↖': "\\", '↗': "/", '↙': "/", '↘': "\\",

	// Chart: blocks and the axis rule.
	'█': "#", '▇': "#", '▆': "=", '▅': "=",
	'▄': "_", '▃': ":", '▂': ".", '▁': ".",
	'│': "|",
}

// Transliterate maps non-ASCII characters to their ASCII equivalents where one
// is defined, leaving ASCII alone. For contexts that must not carry diacritics
// at all -- a generated filename, say -- where "Góra" should become "Gora"
// rather than losing the letter.
func Transliterate(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		switch {
		case r < 128:
			b.WriteRune(r)
		default:
			if sub, ok := asciiMap[r]; ok {
				b.WriteString(sub)
			}
		}
	}
	return b.String()
}

// ASCII rewrites output to pure ASCII, one character for one character.
//
// The point is SMS: a single non-ASCII character forces the whole message from
// GSM-7 into UCS-2, which cuts a segment from 160 characters to 70. A degree
// sign alone therefore more than doubles the cost of sending a forecast.
//
// The degree sign becomes the unit letter, which is both ASCII and clearer to
// someone reading it cold: "29C" rather than "29".
func ASCII(s, units string) string {
	degree := "C"
	if units == "imperial" {
		degree = "F"
	}
	runes := []rune(s)
	var b strings.Builder
	b.Grow(len(s))
	for i, r := range runes {
		switch {
		case r == '°':
			// Prose from IMGW already writes "30°C"; appending the unit again
			// would give "30CC". Only a bare degree sign gains the letter.
			if i+1 < len(runes) && (runes[i+1] == 'C' || runes[i+1] == 'F') {
				continue
			}
			b.WriteString(degree)
		case r < 128:
			b.WriteRune(r)
		default:
			if sub, ok := asciiMap[r]; ok {
				b.WriteString(sub)
			} else {
				// Anything unmapped -- a place name in another script, a weather
				// code description we have not transliterated -- becomes '?'
				// rather than silently vanishing and misaligning the row.
				b.WriteString("?")
			}
		}
	}
	return b.String()
}