Hey Guys!
Yet another small mini-project for making a currency converter in Go!
It's a simple but fun project that should take someone about an hour or 2 depending on their experience. It takes a currency type from one end, the currency which we intend to convert to and the amount to be converted.
I'm making use of a third-party service (https://openexchangerates.org
) to retrieve the latest currency data.
My Main base currencies are:
1) USD
2) EUR
3) GBP
4) JPY
and I also have support for "other" currencies through input in the TUI. Both for base currencies and currencies to be converted to.
~ Source Code: Found here
Let's Begin
What is your base currency?
List
$ USD
United States Dollar
£ GBP
British Pound
€ EUR
Euro
¥ JPY
Japanese Yen
•••
The main functionalities of the application are:
- Get Conversion details entered by the user
- Use those details and send an API request to Openxchangerates (Third-Party service with the latest currency conversion rates)
- Convert currencies & amount
- Output currencies & amount data to the user
The packages used:
-
net/http
- for http requests to the currency exchange api -
github.com/charmbracelet/huh
- for the TUI interface form -
github.com/charmbracelet/bubbles/list
- feature-rich for browsing a general-purpose list of items -
encoding/json
- in order to marshal the data for the API -
github.com/charmbracelet/lipgloss
- Style definitions for terminal layouts
How does it work?
So let's discuss the first bit of functionality, which is getting conversion details from the user.
A view method for getting this user-provided data had to be made and it asks the user questions on what currency to convert, which currency to be converted to, and more.
func (m model) View() string {
if m.err != nil {
return fmt.Sprintf("Error: %v\n\nPress any key to continue.\n", m.err)
}
if m.finished {
// Return an empty string when finished to avoid redundant output.
return ""
}
switch m.stage {
case 0:
if m.isCustomInput {
return questionStyle.Render("Enter your custom base currency code (e.g., USD):\n\n") + m.textInput.View()
}
return questionStyle.Render("What is your base currency?\n\n") + m.list.View()
case 1:
if m.isCustomInput {
return questionStyle.Render("Enter your custom target currency code (e.g., EUR):\n\n") + m.textInput.View()
}
return questionStyle.Render("What do you want to convert to?\n\n") + m.list.View()
case 2:
return questionStyle.Render("How much to convert?\n\n") + m.textInput.View()
default:
return ""
}
}
What do you want to convert to?
List
$ USD
United States Dollar
£ GBP
British Pound
•••
How much to convert?
> 200
Now let's discuss the second point, using the currency conversion details and sending an API request to Openxchangerates.
Here I'm getting/fetching for the latest currency rates from Openxchangerates.org via an API key provided by the third-party currency exchange platform. In my case, I made use of .env's
for secret management but there are a multitude of other ways to better handle this, especially if it were a production app.
~ Openxchangerates.org docs used for this: located here
//api.go
package api
import (
"encoding/json"
"fmt"
"net/http"
)
type CurrencyData struct {
Base string `json:"base"`
Rates map[string]float64 `json:"rates"`
}
func FetchRates(apiKey string) (CurrencyData, error) {
url := fmt.Sprintf("https://openexchangerates.org/api/latest.json?app_id=%s&prettyprint=false", apiKey)
resp, err := http.Get(url)
if err != nil {
return CurrencyData{}, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return CurrencyData{}, fmt.Errorf("API request failed with status: %s", resp.Status)
}
var data CurrencyData
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return CurrencyData{}, err
}
return data, nil
}
Then we'll proceed with converting the currencies and amounts:
//conversion.go
package conversion
func Convert(amount float64, rateFrom, rateTo float64) float64 {
return amount * (rateTo / rateFrom)
}
which is simply taking in a base amount (amount), a rate from the base currency and the rate to be converted to and returns a final converted amount.
Lastly, providing the converted currencies and amount-related data back to the user.
Controlling the logic behind these selections is an update function that serves as the main state transition handler for the application's model.
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "enter":
if m.isCustomInput {
// Handle custom currency input
input := strings.ToUpper(strings.TrimSpace(m.textInput.Value()))
if !isAlphabetic(input) || len(input) != 3 {
m.err = fmt.Errorf("invalid currency code: must be 3 letters")
return m, nil
}
if m.stage == 0 {
m.currencyFrom = input
m.isCustomInput = false
m.textInput.Reset()
m.stage++
m.list.ResetSelected()
return m, nil
} else if m.stage == 1 {
m.currencyTo = input
m.isCustomInput = false
m.textInput.Reset()
m.stage++
m.textInput.Placeholder = "Enter amount (e.g., 100)"
m.textInput.Focus()
return m, nil
}
} else if m.stage == 2 {
// Handle amount input
amountStr := strings.TrimSpace(m.textInput.Value())
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
m.err = fmt.Errorf("invalid amount: %v", err)
return m, nil
}
m.amount = amount
m.finished = true
return m, tea.Quit
} else {
// Handle list selection
selectedItem := m.list.SelectedItem().(Item)
if selectedItem.Code == "OTHER" {
m.isCustomInput = true
m.textInput.Placeholder = "Enter currency code (e.g., USD)"
m.textInput.Focus()
return m, textinput.Blink
}
if m.stage == 0 {
m.currencyFrom = selectedItem.Code
m.stage++
m.list.ResetSelected()
return m, nil
} else if m.stage == 1 {
m.currencyTo = selectedItem.Code
m.stage++
m.textInput.Placeholder = "Enter amount (e.g., 100)"
m.textInput.Focus()
return m, nil
}
}
case "ctrl+c", "esc":
return m, tea.Quit
}
}
// Handle text input for custom currency or amount input
if m.isCustomInput || m.stage == 2 {
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
// Handle list updates for currency selection
if m.stage == 0 || m.stage == 1 {
m.list, cmd = m.list.Update(msg)
return m, cmd
}
return m, nil
}
// go run main.go
200.00 EUR = 166.03 GBP
Conclusion
That pretty much wraps up this relatively quick currency converter. I hope you've enjoyed the quick read and feel free to give a shot also, it's not that bad! 😁.
Feel free to also experiment with other third-party currency exchange providers out there, there are many. Hopefully, they got a decent API too!
See you guys on the next one! 👋🏼
Top comments (0)