package exchangerates import ( "bytes" "encoding/xml" "math" "net/http" "time" "golang.org/x/net/html/charset" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/validators" ) const swissNationalBankExchangeRateUrl = "https://www.snb.ch/public/en/rss/exchangeRates" const swissNationalBankExchangeRateReferenceUrl = "https://www.snb.ch/en/the-snb/mandates-goals/statistics/statistics-pub/current_interest_exchange_rates" const swissNationalBankDataSource = "Schweizerische Nationalbank" const swissNationalBankBaseCurrency = "CHF" const swissNationalBankDataUpdateDateFormat = "Mon, 02 Jan 2006 15:04:05 MST" const swissNationalBankExchangeRatePeriodDateFormat = "2006-01-02" // SwissNationalBankDataSource defines the structure of exchange rates data source of the reserve Swiss National Bank type SwissNationalBankDataSource struct { HttpExchangeRatesDataSource } // SwissNationalBankData represents the whole data from the reserve Swiss National Bank type SwissNationalBankData struct { XMLName xml.Name `xml:"rss"` Channel *SwissNationalBankRssChannel `xml:"channel"` } // SwissNationalBankRssChannel represents the rss channel from the reserve Swiss National Bank type SwissNationalBankRssChannel struct { PublishDate string `xml:"pubDate"` Items []*SwissNationalBankChannelItem `xml:"item"` } // SwissNationalBankChannelItem represents the channel item from the reserve Swiss National Bank type SwissNationalBankChannelItem struct { Statistics *SwissNationalBankItemStatistics `xml:"statistics"` } // SwissNationalBankItemStatistics represents the item statistics from the reserve Swiss National Bank type SwissNationalBankItemStatistics struct { ExchangeRate *SwissNationalBankExchangeRate `xml:"exchangeRate"` } // SwissNationalBankExchangeRate represents the exchange rate from the reserve Swiss National Bank type SwissNationalBankExchangeRate struct { BaseCurrency string `xml:"baseCurrency"` TargetCurrency string `xml:"targetCurrency"` Observation *SwissNationalBankExchangeRateObservation `xml:"observation"` ObservationPeriod *SwissNationalBankExchangeRateObservationPeriod `xml:"observationPeriod"` } // SwissNationalBankExchangeRateObservation represents the exchange rate data from the reserve Swiss National Bank type SwissNationalBankExchangeRateObservation struct { Value string `xml:"value"` Unit string `xml:"unit"` UnitExponent string `xml:"unit_mult"` } // SwissNationalBankExchangeRateObservationPeriod represents the exchange rate period data from the reserve Swiss National Bank type SwissNationalBankExchangeRateObservationPeriod struct { Period string `xml:"period"` } // ToLatestExchangeRateResponse returns a view-object according to original data from the reserve Swiss National Bank func (e *SwissNationalBankData) ToLatestExchangeRateResponse(c core.Context) *models.LatestExchangeRateResponse { if e.Channel == nil { log.Errorf(c, "[swiss_national_bank_datasource.ToLatestExchangeRateResponse] rss channel does not exist") return nil } if len(e.Channel.Items) < 1 { log.Errorf(c, "[swiss_national_bank_datasource.ToLatestExchangeRateResponse] channel items is empty") return nil } latestCurrencyExchangeRateDate := make(map[string]int64) latestExchangeRates := make(map[string]*models.LatestExchangeRate) for i := 0; i < len(e.Channel.Items); i++ { item := e.Channel.Items[i] if item.Statistics == nil || item.Statistics.ExchangeRate == nil || item.Statistics.ExchangeRate.Observation == nil || item.Statistics.ExchangeRate.ObservationPeriod == nil { continue } if item.Statistics.ExchangeRate.BaseCurrency != swissNationalBankBaseCurrency || item.Statistics.ExchangeRate.Observation.Unit != swissNationalBankBaseCurrency { continue } if _, exists := validators.AllCurrencyNames[item.Statistics.ExchangeRate.TargetCurrency]; !exists { continue } date, err := time.Parse(swissNationalBankExchangeRatePeriodDateFormat, item.Statistics.ExchangeRate.ObservationPeriod.Period) if err != nil { log.Warnf(c, "[swiss_national_bank_datasource.ToLatestExchangeRateResponse] failed to parse exchange rate period date, period is %s", item.Statistics.ExchangeRate.ObservationPeriod.Period) continue } currency := item.Statistics.ExchangeRate.TargetCurrency latestDate, exists := latestCurrencyExchangeRateDate[currency] if !exists || date.Unix() > latestDate { finalExchangeRate := item.Statistics.ExchangeRate.ToLatestExchangeRate(c) if finalExchangeRate != nil { latestCurrencyExchangeRateDate[currency] = date.Unix() latestExchangeRates[currency] = finalExchangeRate } } } exchangeRates := make(models.LatestExchangeRateSlice, 0, len(e.Channel.Items)) for _, exchangeRate := range latestExchangeRates { exchangeRates = append(exchangeRates, exchangeRate) } updateDateTime := e.Channel.PublishDate updateTime, err := time.Parse(swissNationalBankDataUpdateDateFormat, updateDateTime) if err != nil { log.Errorf(c, "[swiss_national_bank_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime) return nil } latestExchangeRateResp := &models.LatestExchangeRateResponse{ DataSource: swissNationalBankDataSource, ReferenceUrl: swissNationalBankExchangeRateReferenceUrl, UpdateTime: updateTime.Unix(), BaseCurrency: swissNationalBankBaseCurrency, ExchangeRates: exchangeRates, } return latestExchangeRateResp } // ToLatestExchangeRate returns a data pair according to original data from the reserve Swiss National Bank func (e *SwissNationalBankExchangeRate) ToLatestExchangeRate(c core.Context) *models.LatestExchangeRate { rate, err := utils.StringToFloat64(e.Observation.Value) if err != nil { log.Warnf(c, "[swiss_national_bank_datasource.ToLatestExchangeRate] failed to parse rate, currency is %s, rate is %s", e.TargetCurrency, e.Observation.Value) return nil } if rate <= 0 { log.Warnf(c, "[swiss_national_bank_datasource.ToLatestExchangeRate] rate is invalid, currency is %s, rate is %s", e.TargetCurrency, e.Observation.Value) return nil } unitExponent, err := utils.StringToInt(e.Observation.UnitExponent) if err != nil { log.Warnf(c, "[swiss_national_bank_datasource.ToLatestExchangeRate] failed to parse unit, currency is %s, unit exponent is %s", e.TargetCurrency, e.Observation.UnitExponent) return nil } finalRate := 1 / rate if unitExponent > 1 { finalRate = finalRate / math.Pow10(unitExponent-1) } else if unitExponent < 0 { finalRate = finalRate * math.Pow10(-unitExponent) } else if unitExponent == 0 { log.Warnf(c, "[swiss_national_bank_datasource.ToLatestExchangeRate] unit exponent is zero, currency is %s", e.TargetCurrency) return nil } if math.IsInf(finalRate, 0) { return nil } return &models.LatestExchangeRate{ Currency: e.TargetCurrency, Rate: utils.Float64ToString(finalRate), } } // BuildRequests returns the Swiss National Bank exchange rates http requests func (e *SwissNationalBankDataSource) BuildRequests() ([]*http.Request, error) { req, err := http.NewRequest("GET", swissNationalBankExchangeRateUrl, nil) if err != nil { return nil, err } return []*http.Request{req}, nil } // Parse returns the common response entity according to the the reserve Swiss National Bank data source raw response func (e *SwissNationalBankDataSource) Parse(c core.Context, content []byte) (*models.LatestExchangeRateResponse, error) { xmlDecoder := xml.NewDecoder(bytes.NewReader(content)) xmlDecoder.CharsetReader = charset.NewReaderLabel swissNationalBankData := &SwissNationalBankData{} err := xmlDecoder.Decode(swissNationalBankData) if err != nil { log.Errorf(c, "[swiss_national_bank_datasource.Parse] failed to parse xml data, content is %s, because %s", string(content), err.Error()) return nil, errs.ErrFailedToRequestRemoteApi } latestExchangeRateResponse := swissNationalBankData.ToLatestExchangeRateResponse(c) if latestExchangeRateResponse == nil { log.Errorf(c, "[swiss_national_bank_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content)) return nil, errs.ErrFailedToRequestRemoteApi } return latestExchangeRateResponse, nil }