initial commit
This commit is contained in:
parent
7d34e0d6f9
commit
704d2e0330
7 changed files with 1410 additions and 1 deletions
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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:
|
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
13
go.mod
Normal 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
15
go.sum
Normal 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
1092
iso/currencies.go
Normal file
File diff suppressed because it is too large
Load diff
10
iso/currency.go
Normal file
10
iso/currency.go
Normal 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
98
iso/generate.go
Normal 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
181
mycurrencynet.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue