aboutsummaryrefslogtreecommitdiff
path: root/main.go
blob: 458659cfdb49504434de3c80d6eb69a4ca6c098a (plain)
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
// Phelpsify is a tool to help assign prayer codes to prayers.
// It requests prayers from bahaiprayers.net via the API url.
// It reads already assigned prayer codes from rel/code.list.
// It reads the conversion from number to language code from rel/lang.csv.
// It writes the new prayer codes to rel/code.list.
// rel/code.list is structured as prayer code, comma, prayer ids from bahaiprayers.net all separated by commas per line.
// rel/lang.csv is a csv file with header id,iso,iso_type,name,english,flag_link,rtl.
// The tool is a command line tool that first asks which languages you want to complete.
// It then presents you a random prayer from those languages that doesn't have
// a prayer code yet. It will then help you find the prayer among the prayers that already have a prayer code using keyword based search.
// When a match is found, the id of the prayer will be added to the list after the prayer code.
// The tool then asks you if you want to add another prayer and repeat the process.
package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"

	"git.kiefte.eu/lapingvino/prompt"
)

//BPNAPI is the API link of bahaiprayers.net
//It is used to get the list of prayers per language by numerical id from lang.csv
var BPNAPI = "https://bahaiprayers.net/api/prayer/prayersystembylanguage?languageid="

type BPNKind string

type Language struct {
	Id       int
	Iso      string
	IsoType  string
	Name     string
	English  string
	FlagLink string
	Rtl      bool
}

//BPNAPIOutput is the JSON structure of the API output
type BPNAPIOutput struct {
	ErrorMessage string
	IsInError    bool
	Version      int
	Prayers      []Prayer
	Tags         []struct {
		Id          int
		LanguageId  int
		Name        string
		Kind        BPNKind
		PrayerCount int
	}
	TagRelations []struct {
		Id          int
		PrayerId    int
		PrayerTagId int
		LanguageId  int
	}
	Urls      []interface{}
	Languages []struct {
		Id            int
		Name          string
		English       string
		IsLeftToRight bool
		FlagLink      string
	}
}

type Prayer struct {
	Id         int
	AuthorId   int
	LanguageId int
	Text       string
	Tags       []struct {
		Id   int
		Name string
		Kind BPNKind
	}
	Tagkind struct {
		Kind BPNKind
	}
	Urls []interface{}
}

func (p Prayer) Author() string {
	if p.AuthorId > 0 && p.AuthorId < 4 {
		return []string{"Báb", "Bahá'u'lláh", "Abdu'l-Bahá"}[p.AuthorId-1]
	}
	return "Unknown Author"
}

type PrayerCode struct {
	Code     string
	Language string
}

var Languages map[int]Language
var CodeList map[int]string
var PrayersWithCode map[PrayerCode]Prayer

// ReadLangCSV reads rel/lang.csv and puts it in Languages
// It does so by matching each CSV field to a struct field
func ReadLangCSV() error {
	file, err := os.Open("rel/lang.csv")
	if err != nil {
		return err
	}
	defer file.Close()
	reader := csv.NewReader(file)
	langCSV, err := reader.ReadAll()
	if err != nil {
		return err
	}
	Languages = make(map[int]Language)
	for _, lang := range langCSV[1:] {
		var language Language
		language.Id, _ = strconv.Atoi(lang[0])
		language.Iso = lang[1]
		language.IsoType = lang[2]
		language.Name = lang[3]
		language.English = lang[4]
		language.FlagLink = lang[5]
		language.Rtl, _ = strconv.ParseBool(lang[6])
		Languages[language.Id] = language
	}
	fmt.Print("Available languages: ")
	var langs []string
	for _, lang := range Languages {
		langs = append(langs, lang.English+" ("+lang.Iso+")")
	}
	sort.Strings(langs)
	fmt.Println(strings.Join(langs, ", "))
	return nil
}

func ReadCodeList(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	CodeList = make(map[int]string)
	reader := csv.NewReader(file)
	reader.FieldsPerRecord = -1
	for {
		line, err := reader.Read()
		if err != nil {
			break
		}
		for _, prayerIDstr := range line[1:] {
			prayerID, err := strconv.Atoi(prayerIDstr)
			if err != nil {
				return err
			}
			CodeList[prayerID] = line[0]
		}
	}
	fmt.Println("Number of prayers done:", len(CodeList))
	return nil
}

func WriteCodeList(codeList map[int]string) error {
	file, err := os.Create("rel/code.list")
	if err != nil {
		return err
	}
	defer file.Close()
	writer := csv.NewWriter(file)
	invertedCodeList := make(map[string][]int)
	for prayerID, code := range codeList {
		invertedCodeList[code] = append(invertedCodeList[code], prayerID)
	}

	var codes []string
	for code := range invertedCodeList {
		codes = append(codes, code)
	}

	sort.Strings(codes)

	for _, code := range codes {
		var line []string
		for _, prayerID := range invertedCodeList[code] {
			line = append(line, strconv.Itoa(prayerID))
		}
		sort.Strings(line)
		line = append([]string{code}, line...)
		err := writer.Write(line)
		if err != nil {
			return err
		}
	}
	writer.Flush()
	return nil
}

func AskLanguages(pr string) []Language {
	fmt.Print(pr)
	var languages []string
	for {
		fmt.Print("Language name or code, leave blank to continue: ")
		s := prompt.MustRead[string]()
		if s == "" {
			break
		}
		languages = append(languages, s)
	}
	var outLangs []Language
	for _, language := range languages {
		for _, lang := range Languages {
			if lang.Iso == language || lang.English == language {
				outLangs = append(outLangs, lang)
			}
		}
	}
	return outLangs
}

func ReadPrayers(lang []Language, codep bool) []Prayer {
	var prayers []Prayer
	for _, language := range lang {
		response, err := http.Get(BPNAPI + strconv.Itoa(language.Id))
		if err != nil {
			fmt.Println(err)
			panic("Could not get prayers, abort")
		}
		defer response.Body.Close()
		var output BPNAPIOutput
		err = json.NewDecoder(response.Body).Decode(&output)
		if err != nil {
			fmt.Println("Issue when reading " + language.English + " prayers: " + err.Error())
			continue
		}
		fmt.Print(language.Iso + "..")
		for _, prayer := range output.Prayers {
			if (CodeList[prayer.Id] != "") == codep {
				prayers = append(prayers, prayer)
			}
		}
		fmt.Println("done")
	}
	return prayers
}

// ByPrayerLength tries to return prayers closest in length to
// the reference prayer r (in characters)
func ByPrayerLength(p []Prayer, r Prayer) []Prayer {
	out := NewPrayerLengthSort(p, r)
	sort.Sort(out)
	return out.Prayers()
}

type PrayerLengthSort []struct {
	P    Prayer
	Diff int
}

func (p PrayerLengthSort) Len() int {
	return len(p)
}

func (p PrayerLengthSort) Less(i, j int) bool {
	return p[i].Diff < p[j].Diff
}

func (p PrayerLengthSort) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

func (p PrayerLengthSort) Sort() {
	sort.Sort(p)
}

func (p PrayerLengthSort) Prayers() []Prayer {
	var out []Prayer
	for _, prayer := range p {
		out = append(out, prayer.P)
	}
	return out
}

func NewPrayerLengthSort(p []Prayer, r Prayer) PrayerLengthSort {
	var out PrayerLengthSort
	for _, prayer := range p {
		diff := len(prayer.Text) - len(r.Text)
		if diff < 0 {
			diff = -diff
		}
		// Add a penalty if the author is not the same
		if prayer.AuthorId != r.AuthorId {
			diff += 100
		}
		out = append(out, struct {
			P    Prayer
			Diff int
		}{prayer, diff})
	}
	return out
}

func main() {
	err := ReadLangCSV()
	if err != nil {
		panic(err)
	}
	err = ReadCodeList("rel/code.list")
	if err != nil {
		panic(err)
	}
	fmt.Print("Do you want to merge in someone else's work? (y/n): ")
	s := prompt.MustRead[string]()
	if s == "y" {
		fmt.Print("Enter the name of the file: ")
		s = prompt.MustRead[string]()
		err = ReadCodeList(s)
		if err != nil {
			fmt.Println("Could not read " + s + ": " + err.Error())
		}
	}
	// Ask which languages to use as a reference and read in all prayers
	// with a language code to PrayersWithCode
	refLanguages := AskLanguages("Which languages do you want to reference to? ")
	prayers := ReadPrayers(refLanguages, true)
	PrayersWithCode = make(map[PrayerCode]Prayer)
	for _, prayer := range prayers {
		code := CodeList[prayer.Id]
		if code != "" {
			PrayersWithCode[PrayerCode{code, Languages[prayer.LanguageId].Iso}] = prayer
		}
	}
	fmt.Println("Number of prayers with code loaded: " + strconv.Itoa(len(PrayersWithCode)))
	// Ask which language to complete
	languages := AskLanguages("Which languages do you want to complete? ")
	prayers = ReadPrayers(languages, false)
	for {
		// randomize the order of the prayers
		for i := len(prayers) - 1; i > 0; i-- {
			j := rand.Intn(i + 1)
			prayers[i], prayers[j] = prayers[j], prayers[i]
		}
		// pick the first prayer from the resulting list that
		// doesn't have a code in CodeList.
		var prayer Prayer
		var code string
		for _, p := range prayers {
			if CodeList[p.Id] == "" {
				prayer = p
				break
			}
		}
		for code == "" {
			// Clear the screen
			fmt.Print("\033[H\033[2J")
			// Present the text, id and author of the prayer
			fmt.Println(prayer.Text)
			fmt.Println("ID:", prayer.Id)
			fmt.Println("Author:", prayer.Author())
			// Ask for a keyword
			fmt.Print("Input a keyword for this prayer, skip to pick another one or code to enter the code manually: ")
			keyword := prompt.MustRead[string]()
			if keyword == "skip" {
				break
			}
			if keyword == "code" {
				fmt.Print("Enter the code: ")
				code = prompt.MustRead[string]()
				break
			}
			var Matches []Prayer
			// Check for the prayer text of each prayer in
			// PrayersWithCode if there is a match with the keyword
			// and add it to Matches
			for _, pr := range PrayersWithCode {
				if strings.Contains(pr.Text, keyword) {
					Matches = append(Matches, pr)
				}
			}
			// If there are no matches, ask again
			if len(Matches) == 0 {
				fmt.Println("No matches found.")
				continue
			}
			// Ask which of the matches to use
			fmt.Println("Found " + strconv.Itoa(len(Matches)) + " matches.")
			fmt.Println("Which of the following matches?")
			sortedMatches := NewPrayerLengthSort(Matches, prayer)
			sortedMatches.Sort()
			for i, match := range sortedMatches.Prayers() {
				fmt.Println(i+1, ":", match.Text)
				fmt.Print("Does this match? (y/n/skip) ")
				answer := prompt.MustRead[string]()
				if answer == "y" {
					fmt.Println(CodeList[match.Id])
					code = CodeList[match.Id]
					break
				}
				if answer == "skip" {
					break
				}
				// If the answer is not y or skip, clear the screen
				fmt.Print("\033[H\033[2J")
				// Present the text, id and author of the prayer again
				fmt.Println(prayer.Text)
				fmt.Println("ID:", prayer.Id)
				fmt.Println("Author:", prayer.Author())
			}
		}
		if code != "" {
			// If the code is not empty, add it to the code list
			// and write the code list to CodeList.csv
			CodeList[prayer.Id] = code
			WriteCodeList(CodeList)
		}
		// Ask if the user wants to identify another prayer
		// or if they want to quit
		fmt.Print("Identify another prayer? (y/n) ")
		answer := prompt.MustRead[string]()
		if answer == "n" {
			break
		}
	}
}