initial commit

main
Yiyang Kang 2022-12-06 20:56:03 +08:00
parent 7d34e0d6f9
commit 704d2e0330
Signed by: kkyy
GPG Key ID: 80FD317ECAF06CC3
7 changed files with 1410 additions and 1 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2022 Yiyang Kang
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

13
go.mod Normal file
View File

@ -0,0 +1,13 @@
module git.gensokyo.cafe/kkyy/mycurrencynet
go 1.19
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/go-resty/resty/v2 v2.7.0
)
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
golang.org/x/net v0.2.0 // indirect
)

15
go.sum Normal file
View File

@ -0,0 +1,15 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

1092
iso/currencies.go Normal file

File diff suppressed because it is too large Load Diff

10
iso/currency.go Normal file
View File

@ -0,0 +1,10 @@
package iso
type ISOCurrency struct {
Name string
Code string
Number string
MinorUnit string
}
//go:generate go run generate.go

98
iso/generate.go Normal file
View File

@ -0,0 +1,98 @@
//go:build ignore
package main
import (
"bytes"
"encoding/xml"
"fmt"
"os"
"os/exec"
"time"
"github.com/go-resty/resty/v2"
)
const (
xmlFileURL = "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml"
)
type iso4217 struct {
XMLName xml.Name `xml:"ISO_4217"`
Published string `xml:"Pblshd,attr"`
CurrencyTable struct {
XMLName xml.Name `xml:"CcyTbl"`
CurrencyEntries []iso4217CurrencyEntry `xml:"CcyNtry"`
} `xml:"CcyTbl"`
}
type iso4217CurrencyEntry struct {
XMLName xml.Name `xml:"CcyNtry"`
CountryName string `xml:"CtryNm"`
CurrencyName string `xml:"CcyNm"`
Currency string `xml:"Ccy"`
CurrencyNumber string `xml:"CcyNbr"`
CurrencyMinorUnits string `xml:"CcyMnrUnts"`
}
func main() {
resp, err := resty.New().
SetTimeout(time.Second * 10).
R().Get(xmlFileURL)
if err != nil {
panic(err)
}
if resp.StatusCode() != 200 {
panic(fmt.Sprintf("response from %q: %d %q", xmlFileURL, resp.StatusCode(), string(resp.Body())))
}
var parsed iso4217
err = xml.Unmarshal(resp.Body(), &parsed)
if err != nil {
panic(err)
}
buf := bytes.NewBuffer(nil)
_, _ = fmt.Fprint(buf, "// Code generated with `go generate`. DO NOT EDIT.\n\n")
_, _ = fmt.Fprint(buf, "package iso\n\n")
_, _ = fmt.Fprint(buf, "var Currencies = map[string]ISOCurrency {\n")
sean := map[string]struct{}{}
for _, entry := range parsed.CurrencyTable.CurrencyEntries {
if entry.Currency == "" {
continue
}
if _, ok := sean[entry.Currency]; ok {
continue
}
sean[entry.Currency] = struct{}{}
_, _ = fmt.Fprintf(buf, "\t%q: {\n", entry.Currency)
_, _ = fmt.Fprintf(buf, "\t\t%s: %q,\n", "Name", entry.CurrencyName)
_, _ = fmt.Fprintf(buf, "\t\t%s: %q,\n", "Code", entry.Currency)
_, _ = fmt.Fprintf(buf, "\t\t%s: %q,\n", "Number", entry.CurrencyNumber)
_, _ = fmt.Fprintf(buf, "\t\t%s: %q,\n", "MinorUnit", entry.CurrencyMinorUnits)
_, _ = fmt.Fprint(buf, "\t},\n")
}
_, _ = fmt.Fprint(buf, "}\n")
oF, err := os.OpenFile("currencies.go", os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
fmtCmd := exec.Command("gofmt", "-s")
fmtCmd.Stdin = bytes.NewReader(buf.Bytes())
fmtCmd.Stdout = oF
if err = fmtCmd.Start(); err != nil {
panic(err)
}
if err = fmtCmd.Wait(); err != nil {
panic(err)
}
}

181
mycurrencynet.go Normal file
View File

@ -0,0 +1,181 @@
package mycurrencynet
import (
"bytes"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/go-resty/resty/v2"
"git.gensokyo.cafe/kkyy/mycurrencynet/iso"
)
var (
updateUrl = "https://www.mycurrency.net/=US"
client = resty.New()
)
type Currency struct {
Code string
Name string
Countries []string // TODO fill this
Rate float64
}
func (c *Currency) To(t *Currency) float64 {
if c == nil || t == nil || c.Rate == 0 {
return 0
}
return t.Rate / c.Rate
}
func init() {
client.
SetTimeout(5*time.Second).
SetRetryCount(1).
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0")
}
func fetch() (html []byte, err error) {
resp, err := client.R().Get(updateUrl)
if err != nil {
return
}
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("HTTP %d %s: %q", resp.StatusCode(), resp.Status(), resp.String())
}
return resp.Body(), nil
}
func parse(body []byte) (map[string]*Currency, error) {
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
if err != nil {
return nil, err
}
ret := make(map[string]*Currency)
doc.Find("tr.country").Each(func(_ int, s *goquery.Selection) {
var (
c Currency
isoC iso.ISOCurrency
ok bool
err error
rate string
)
if c.Code, ok = s.Attr("data-currency-code"); !ok {
return
}
if _, exist := ret[c.Code]; exist {
return
}
if isoC, ok = iso.Currencies[c.Code]; !ok {
return
}
c.Name = isoC.Name
if rate, ok = s.Find(".money[data-rate]").Attr("data-rate"); !ok {
return
}
if c.Rate, err = strconv.ParseFloat(rate, 64); err != nil {
return
}
ret[c.Code] = &c
})
return ret, nil
}
func Fetch() (map[string]*Currency, error) {
html, err := fetch()
if err != nil {
return nil, err
}
rates, err := parse(html)
if err != nil {
return nil, err
}
// add base currency
rates["USD"] = &Currency{
Code: "USD",
Name: iso.Currencies["USD"].Name,
Rate: 1,
}
return rates, nil
}
type MyCurrencyNet struct {
rates map[string]*Currency
lastUpdate time.Time
mu sync.RWMutex
}
func New() *MyCurrencyNet {
return &MyCurrencyNet{}
}
func (m *MyCurrencyNet) Get(codes ...string) ([]*Currency, error) {
if m.rates == nil {
return nil, fmt.Errorf("not initialized.")
}
ret := make([]*Currency, len(codes))
m.mu.RLock()
defer m.mu.RUnlock()
var (
errors []string
err error = nil
)
for i, code := range codes {
rate, ok := m.rates[strings.ToUpper(code)]
if !ok {
errors = append(errors, "unknown currency code: "+code)
continue
}
var cp Currency = *rate
ret[i] = &cp
}
if len(errors) > 0 {
err = fmt.Errorf(strings.Join(errors, ", "))
}
return ret, err
}
func (m *MyCurrencyNet) Update() error {
currencies, err := Fetch()
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
m.rates = currencies
m.lastUpdate = time.Now()
return nil
}
func (m *MyCurrencyNet) UpdateEvery(interval time.Duration, onErr func(error), onSuccess func()) {
for {
<-time.After(interval)
if err := m.Update(); err != nil {
if onErr != nil {
onErr(err)
}
} else {
if onSuccess != nil {
onSuccess()
}
}
}
}