Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pr_status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
- 'Projects/Home/**'
detail:
- 'Projects/Detail/**'
helpers:
- 'Projects/Helpers/**'
- name: Generate matrix
id: matrix
run: |
Expand All @@ -51,6 +53,10 @@ jobs:
if [ "${{ steps.filter.outputs.detail }}" == "true" ]; then
targets+=("Detail")
fi

if [ "${{ steps.filter.outputs.helpers }}" == "true" ]; then
targets+=("Helpers")
fi

if [ ${#targets[@]} -gt 0 ]; then
matrix="{\"target\": [$(printf '"%s",' "${targets[@]}" | sed 's/,$//')]}"
Expand Down
2 changes: 1 addition & 1 deletion Projects/Detail/Sources/Model/ExchangeDetailModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct ExchangeDetailModel: Equatable {
let name: String
let description: String
let logoUrl: String
let spotVolumeUsd: Double?
let spotVolumeUsd: String
let makerFee: Double
let takerFee: Double
let dateLaunched: String
Expand Down
2 changes: 1 addition & 1 deletion Projects/Detail/Sources/View/DetailHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ final class DetailHeaderView: UIView {
func configure(with model: ExchangeDetailModel) {
titleLabel.text = "\(model.name) - ID \(model.id)"
descriptionLabel.text = model.description
dateLaunchedLabel.text = "Date launched: \(model.dateLaunched)"
dateLaunchedLabel.text = model.dateLaunched
makerFeeLabel.text = "Maker: \(formatPercentage(model.makerFee))"
takerFeeLabel.text = "Taker: \(formatPercentage(model.takerFee))"
loadImage(from: model.logoUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ extension DetailCoordinatorTests {
name: "Binance",
description: "Binance Exchange",
logo: "https://logo.url",
spotVolumeUsd: 1000000.0,
spotVolumeUsd: "$ 1.000.000,0",
makerFee: 0.1,
takerFee: 0.2,
dateLaunched: "2017-07-14",
Expand Down
2 changes: 1 addition & 1 deletion Projects/Detail/Tests/View/DetailViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ extension DetailViewControllerTests {
name: "Test Exchange",
description: "Test Description",
logoUrl: "https://test.com/logo.png",
spotVolumeUsd: 1000000,
spotVolumeUsd: "$ 1.000.000,0",
makerFee: 0.1,
takerFee: 0.2,
dateLaunched: "2020-01-01",
Expand Down
2 changes: 1 addition & 1 deletion Projects/Detail/Tests/ViewModel/DetailViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ extension DetailViewModelTests {
name: String = "Binance",
description: String? = "Binance Exchange",
logo: String = "https://logo.url",
spotVolumeUsd: Double? = 1000000.0,
spotVolumeUsd: String = "$ 1.000.000,0",
makerFee: Double = 0.1,
takerFee: Double = 0.2,
dateLaunched: String = "2017-07-14",
Expand Down
7 changes: 7 additions & 0 deletions Projects/Helpers/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ProjectDescription
import ProjectDescriptionHelpers

let moduleName = "Helpers"

let project = Project.templateModule(named: moduleName,
targets: [.source, .test])
32 changes: 32 additions & 0 deletions Projects/Helpers/Sources/NumbersFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// NumberFormatter.swift
// Helpers
//
// Created by Vitor Conceicao on 23/02/26.
//

import Foundation

public struct NumbersFormatter {
let locale: Locale

public init(locale: Locale = .current) {
self.locale = locale
}

public func formatPrice(_ value: Double?) -> String {
guard let value else { return "-" }
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "$"
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
formatter.usesGroupingSeparator = true
formatter.groupingSeparator = "."
formatter.decimalSeparator = ","

formatter.locale = locale

return formatter.string(from: NSNumber(value: value)) ?? "-"
}
}
32 changes: 32 additions & 0 deletions Projects/Helpers/Sources/StringFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// DateStringFormatter.swift
// Helpers
//
// Created by Vitor Conceicao on 23/02/26.
//

import Foundation

public struct StringFormatter {
let locale: Locale

public init(locale: Locale = .current) {
self.locale = locale
}

public func formatDate(_ date: String) -> String {
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

guard let date = isoFormatter.date(from: date) else {
return date
}

let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .short
displayFormatter.timeStyle = .none
displayFormatter.locale = locale

return displayFormatter.string(from: date)
}
}
53 changes: 53 additions & 0 deletions Projects/Helpers/Tests/NumbersFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// NumbersFormatterTests.swift
// Helpers
//
// Created by Vitor Conceicao on 23/02/26.
//

import Foundation
import Testing

@testable import Helpers

@Suite("StringFormatter Tests")
struct NumbersFormatterTests {
@Test("GIVEN a value WHEN formatPrice with pt_BR locale THEN returns formatted currency string",
arguments: [
(1000000.50, "1.000.000,50"),
(0.0, "0,00"),
(1234.5, "1.234,50")
])
func testFormatPriceWithPtBR(value: Double, expectedString: String) {
let locale = Locale(identifier: "pt_BR")
let formatter = NumbersFormatter(locale: locale)

let result = formatter.formatPrice(value)

#expect(result.contains(expectedString))
}

@Test("GIVEN a value WHEN formatPrice with en_US locale THEN returns formatted currency string",
arguments: [
(1000000.50, "1,000,000.50"),
(0.0, "0.00"),
(1234.5, "1,234.50")
])
func testFormatPriceEnUs(value: Double, expectedString: String) {
let locale = Locale(identifier: "en_US")
let formatter = NumbersFormatter(locale: locale)

let result = formatter.formatPrice(value)

#expect(result.contains(expectedString))
}

@Test("GIVEN a nil value WHEN formatPrice THEN returns default value")
func testNilValue() {
let formatter = NumbersFormatter()

let result = formatter.formatPrice(nil)

#expect(result == "-")
}
}
52 changes: 52 additions & 0 deletions Projects/Helpers/Tests/StringFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// StringFormatter.swift
// Helpers
//
// Created by Vitor Conceicao on 23/02/26.
//

import Foundation
import Testing

@testable import Helpers

@Suite("StringFormatter Tests")
struct StringFormatterTests {

@Test("GIVEN a valid ISO8601 date string WHEN formatDate is called with custom locale THEN returns formatted date for that locale")
func testFormatDateWithCustomLocale() {
let dateString = "2026-02-23T15:30:00.000Z"
let usLocale = Locale(identifier: "en_US")
let expected = "2/23/26"

let result = StringFormatter(locale: usLocale).formatDate(dateString)

#expect(result == expected)
}

@Test("GIVEN a date string with different locale WHEN formatDate is called with pt_BR locale THEN returns formatted date for Brazilian Portuguese")
func testFormatDateWithBrazilianLocale() {
let dateString = "2026-02-23T10:00:00.000Z"
let ptBRLocale = Locale(identifier: "pt_BR")

let result = StringFormatter(locale: ptBRLocale).formatDate(dateString)

let expected = "23/02/2026"

#expect(result == expected)
}

@Test("GIVEN various invalid date strings WHEN formatDate is called THEN returns the original strings",
arguments: [
"invalid-date",
"2020-13-01",
"not a date at all",
"12345",
"2020/01/01"
])
func testFormatDateVariousInvalidDates(dateString: String) {
let result = StringFormatter().formatDate(dateString)

#expect(result == dateString)
}
}
6 changes: 3 additions & 3 deletions Projects/Home/Interfaces/Model/Exchange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct Exchange: Equatable, Identifiable {
public let name: String
public let description: String?
public let logo: String
public let spotVolumeUsd: Double?
public let spotVolumeUsd: String
public let makerFee: Double
public let takerFee: Double
public let dateLaunched: String
Expand All @@ -26,7 +26,7 @@ public struct Exchange: Equatable, Identifiable {
name: String,
description: String? = nil,
logo: String,
spotVolumeUsd: Double? = nil,
spotVolumeUsd: String = "",
makerFee: Double,
takerFee: Double,
dateLaunched: String,
Expand All @@ -50,7 +50,7 @@ public struct Exchange: Equatable, Identifiable {
self.name = summary.name
self.description = nil
self.logo = ""
self.spotVolumeUsd = nil
self.spotVolumeUsd = ""
self.makerFee = 0.0
self.takerFee = 0.0
self.dateLaunched = ""
Expand Down
2 changes: 2 additions & 0 deletions Projects/Home/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ let dependecies: [TargetDependency] = [
path: "../DesignSystem"),
.project(target: "DetailInterfaces",
path: "../Detail"),
.project(target: "Helpers",
path: "../Helpers"),
.external(name: "NetworkingInterfaces"),
]

Expand Down
5 changes: 4 additions & 1 deletion Projects/Home/Sources/Coordinator/HomeCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import DependencyInjectionInterfaces
import DetailInterfaces
import Helpers
import HomeInterfaces
import NavigationInterfaces
import NetworkingInterfaces
Expand All @@ -28,7 +29,9 @@ public final class HomeCoordinator: HomeCoordinating {
let resolver = SharedContainer.shared.resolver()
let networkService: NetworkServiceProtocol = resolver.resolve()
let service = HomeService(networkService: networkService)
let viewModel = HomeViewModel(service: service)
let viewModel = HomeViewModel(service: service,
stringFormatter: .init(),
numbersFormatter: .init())
viewModel.coordinatorDelegate = self
let homeVC = HomeViewController(viewModel: viewModel)
navigationController.pushViewController(homeVC, animated: false)
Expand Down
2 changes: 0 additions & 2 deletions Projects/Home/Sources/Protocols/HomeViewModelProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,4 @@ protocol HomeViewModelProtocol: AnyObject {
func loadData()
func item(at index: Int) -> Exchange
func didSelectRow(at index: Int)
func formatPrice(_ value: Double) -> String
func formatDate(_ date: String) -> String
}
5 changes: 3 additions & 2 deletions Projects/Home/Sources/View/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import DesignSystem
import Helpers
import UIKit

class HomeViewController: UIViewController {
Expand Down Expand Up @@ -142,8 +143,8 @@ extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
if item.isLoadingDetails {
cell.configure(state: .partialLoaded(.init(title: item.name)))
} else {
let volPrice = "Vol: \(viewModel.formatPrice(item.spotVolumeUsd ?? 0.0))"
let dateLaunched = "Date launched: \(viewModel.formatDate(item.dateLaunched))"
let volPrice = "Vol: \(item.spotVolumeUsd)"
let dateLaunched = item.dateLaunched
cell.configure(state: .loaded(.init(url: item.logo,
title: item.name,
subtitle: volPrice,
Expand Down
Loading
Loading