From 4b6324dba64ce494a66e8f17c09d4322a2be2b81 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 15 Mar 2021 23:40:11 +0800 Subject: [PATCH] add Bank of Canada exchange rate data source --- conf/lab.ini | 2 +- .../bank_of_canada_datasource.go | 155 ++++++++++++++++++ .../exchange_rates_datasource_container.go | 3 + pkg/settings/setting.go | 3 + 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 pkg/exchangerates/bank_of_canada_datasource.go diff --git a/conf/lab.ini b/conf/lab.ini index f2ddee9f..f26f3c5c 100644 --- a/conf/lab.ini +++ b/conf/lab.ini @@ -111,7 +111,7 @@ enable_register = true enable_export = true [exchange_rates] -# Exchange rates data source, supports "euro_central_bank", "czech_national_bank", "national_bank_of_poland" currently +# Exchange rates data source, supports "euro_central_bank", "bank_of_canada", "czech_national_bank", "national_bank_of_poland" currently data_source = euro_central_bank # Requesting exchange rates data timeout (milliseconds), default is 10000 (10 seconds) diff --git a/pkg/exchangerates/bank_of_canada_datasource.go b/pkg/exchangerates/bank_of_canada_datasource.go new file mode 100644 index 00000000..7d0566a2 --- /dev/null +++ b/pkg/exchangerates/bank_of_canada_datasource.go @@ -0,0 +1,155 @@ +package exchangerates + +import ( + "encoding/json" + "math" + "strings" + "time" + + "github.com/mayswind/lab/pkg/core" + "github.com/mayswind/lab/pkg/errs" + "github.com/mayswind/lab/pkg/log" + "github.com/mayswind/lab/pkg/models" + "github.com/mayswind/lab/pkg/utils" + "github.com/mayswind/lab/pkg/validators" +) + +const bankOfCanadaExchangeRateUrl = "https://www.bankofcanada.ca/valet/observations/group/FX_RATES_DAILY/json?recent=1" +const bankOfCanadaExchangeRateReferenceUrl = "https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/" +const bankOfCanadaDataSource = "Bank of Canada" +const bankOfCanadaBaseCurrency = "CAD" + +const bankOfCanadaDataUpdateDateFormat = "2006-01-02 15:04" +const bankOfCanadaDataUpdateDateTimezone = "America/Toronto" + +// BankOfCanadaDataSource defines the structure of exchange rates data source of bank of Canada +type BankOfCanadaDataSource struct { + ExchangeRatesDataSource +} + +// BankOfCanadaExchangeRateData represents the whole data from bank of Canada +type BankOfCanadaExchangeRateData struct { + Observations []BankOfCanadaObservationData `json:"observations"` +} + +// BankOfCanadaObservationData represents the observation data from bank of Canada +type BankOfCanadaObservationData map[string]interface{} + +// ToLatestExchangeRateResponse returns a view-object according to original data from bank of Canada +func (e *BankOfCanadaExchangeRateData) ToLatestExchangeRateResponse(c *core.Context) *models.LatestExchangeRateResponse { + if len(e.Observations) < 1 { + log.ErrorfWithRequestId(c, "[bank_of_canada_datasource.ToLatestExchangeRateResponse] observations is empty") + return nil + } + + exchangeRateMap := make(map[string]string) + latestUpdateDate := "" + + for i := 0; i < len(e.Observations); i++ { + observation := e.Observations[i] + updateDateData := observation["d"] + + if updateDate, ok := updateDateData.(string); ok { + if latestUpdateDate == "" || strings.Compare(updateDate, latestUpdateDate) > 0 { + latestUpdateDate = updateDate + } + } + + for typeName, exchangeRateData := range observation { + if len(typeName) < 8 || !strings.HasPrefix(typeName, "FX") || !strings.HasSuffix(typeName, bankOfCanadaBaseCurrency) { + continue + } + + currencyCode := utils.SubString(typeName, 2, 3) + + if data, ok := exchangeRateData.(map[string]interface{}); ok { + exchangeRate := data["v"] + + if exchangeRateValue, ok2 := exchangeRate.(string); ok2 { + exchangeRateMap[currencyCode] = exchangeRateValue + } + } + } + } + + exchangeRates := make(models.LatestExchangeRateSlice, 0, len(exchangeRateMap)) + + for currencyCode, exchangeRate := range exchangeRateMap { + if _, exists := validators.AllCurrencyNames[currencyCode]; !exists { + continue + } + + rate, err := utils.StringToFloat64(exchangeRate) + + if err != nil { + log.WarnfWithRequestId(c, "[bank_of_canada_datasource.ToLatestExchangeRateResponse] failed to parse rate, rate is %s", exchangeRate) + continue + } + + if rate <= 0 { + log.WarnfWithRequestId(c, "[bank_of_canada_datasource.ToLatestExchangeRateResponse] rate is invalid, rate is %s", exchangeRate) + continue + } + + finalRate := 1 / rate + + if math.IsInf(finalRate, 0) { + continue + } + + exchangeRates = append(exchangeRates, &models.LatestExchangeRate{ + Currency: currencyCode, + Rate: utils.Float64ToString(finalRate), + }) + } + + timezone, err := time.LoadLocation(bankOfCanadaDataUpdateDateTimezone) + + if err != nil { + log.ErrorfWithRequestId(c, "[bank_of_canada_datasource.ToLatestExchangeRateResponse] failed to get timezone, timezone name is %s", bankOfCanadaDataUpdateDateTimezone) + return nil + } + + updateDateTime := latestUpdateDate + " 16:30" // Daily average exchange rates - published once each business day by 16:30 ET. + updateTime, err := time.ParseInLocation(bankOfCanadaDataUpdateDateFormat, updateDateTime, timezone) + + if err != nil { + log.ErrorfWithRequestId(c, "[bank_of_canada_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime) + return nil + } + + latestExchangeRateResp := &models.LatestExchangeRateResponse{ + DataSource: bankOfCanadaDataSource, + ReferenceUrl: bankOfCanadaExchangeRateReferenceUrl, + UpdateTime: updateTime.Unix(), + BaseCurrency: bankOfCanadaBaseCurrency, + ExchangeRates: exchangeRates, + } + + return latestExchangeRateResp +} + +// GetRequestUrls returns the bank of Canada data source urls +func (e *BankOfCanadaDataSource) GetRequestUrls() []string { + return []string{bankOfCanadaExchangeRateUrl} +} + +// Parse returns the common response entity according to the bank of Canada data source raw response +func (e *BankOfCanadaDataSource) Parse(c *core.Context, content []byte) (*models.LatestExchangeRateResponse, error) { + bankOfCanadaData := &BankOfCanadaExchangeRateData{} + err := json.Unmarshal(content, bankOfCanadaData) + + if err != nil { + log.ErrorfWithRequestId(c, "[bank_of_canada_datasource.Parse] failed to parse json data, content is %s, because %s", string(content), err.Error()) + return nil, errs.ErrFailedToRequestRemoteApi + } + + latestExchangeRateResponse := bankOfCanadaData.ToLatestExchangeRateResponse(c) + + if latestExchangeRateResponse == nil { + log.ErrorfWithRequestId(c, "[bank_of_canada_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content)) + return nil, errs.ErrFailedToRequestRemoteApi + } + + return latestExchangeRateResponse, nil +} diff --git a/pkg/exchangerates/exchange_rates_datasource_container.go b/pkg/exchangerates/exchange_rates_datasource_container.go index 8863144a..90d2f90d 100644 --- a/pkg/exchangerates/exchange_rates_datasource_container.go +++ b/pkg/exchangerates/exchange_rates_datasource_container.go @@ -20,6 +20,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error { if config.ExchangeRatesDataSource == settings.EuroCentralBankDataSource { Container.Current = &EuroCentralBankDataSource{} return nil + } else if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource { + Container.Current = &BankOfCanadaDataSource{} + return nil } else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource { Container.Current = &CzechNationalBankDataSource{} return nil diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index 4a60d1ab..85931aff 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -65,6 +65,7 @@ const ( // Exchange rates data source types const ( EuroCentralBankDataSource string = "euro_central_bank" + BankOfCanadaDataSource string = "bank_of_canada" CzechNationalBankDataSource string = "czech_national_bank" NationalBankOfPolandDataSource string = "national_bank_of_poland" ) @@ -414,6 +415,8 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error { if getConfigItemStringValue(configFile, sectionName, "data_source") == EuroCentralBankDataSource { config.ExchangeRatesDataSource = EuroCentralBankDataSource + } else if getConfigItemStringValue(configFile, sectionName, "data_source") == BankOfCanadaDataSource { + config.ExchangeRatesDataSource = BankOfCanadaDataSource } else if getConfigItemStringValue(configFile, sectionName, "data_source") == CzechNationalBankDataSource { config.ExchangeRatesDataSource = CzechNationalBankDataSource } else if getConfigItemStringValue(configFile, sectionName, "data_source") == NationalBankOfPolandDataSource {