diff --git a/StockMonitor/Utils/KoreanStockID.swift b/StockMonitor/Utils/KoreanStockID.swift new file mode 100644 index 0000000..d2e21f3 --- /dev/null +++ b/StockMonitor/Utils/KoreanStockID.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Helpers for converting between Stockbar's internal Korean stock ID +/// (`kr_<6digit>.ks` for KOSPI, `kr_<6digit>.kq` for KOSDAQ) and Yahoo +/// Finance symbol (`005930.KS`, `293490.KQ`). +enum KoreanStockID { + + /// Stockbar-internal ID prefix for Korean stocks. + static let prefix = "kr_" + + /// Returns true if `id` looks like a Korean stock ID. Case-insensitive on the prefix. + static func isKorean(_ id: String) -> Bool { + let lower = id.lowercased() + guard lower.hasPrefix(prefix) else { return false } + return lower.count > prefix.count + } + + /// Convert internal ID → Yahoo symbol. Returns nil for non-Korean IDs. + /// e.g. `kr_005930.ks` → `005930.KS` + static func toYahooSymbol(_ id: String) -> String? { + guard isKorean(id) else { return nil } + let lower = id.lowercased() + let suffix = String(lower.dropFirst(prefix.count)) + return suffix.uppercased() + } + + /// Convert Yahoo symbol → internal ID. Always succeeds. + /// e.g. `005930.KS` → `kr_005930.ks` + static func fromYahooSymbol(_ symbol: String) -> String { + return prefix + symbol.lowercased() + } +} diff --git a/StockMonitorTests/Utils/KoreanStockIDTests.swift b/StockMonitorTests/Utils/KoreanStockIDTests.swift new file mode 100644 index 0000000..ec84d47 --- /dev/null +++ b/StockMonitorTests/Utils/KoreanStockIDTests.swift @@ -0,0 +1,61 @@ +import XCTest +@testable import Stockbar + +final class KoreanStockIDTests: XCTestCase { + + // MARK: - isKoreanID + + func test_isKoreanID_kospi() { + XCTAssertTrue(KoreanStockID.isKorean("kr_005930.ks")) + } + + func test_isKoreanID_kosdaq() { + XCTAssertTrue(KoreanStockID.isKorean("kr_293490.kq")) + } + + func test_isKoreanID_rejects_other_markets() { + XCTAssertFalse(KoreanStockID.isKorean("sh600000")) + XCTAssertFalse(KoreanStockID.isKorean("hk00700")) + XCTAssertFalse(KoreanStockID.isKorean("usr_aapl")) + } + + func test_isKoreanID_rejects_empty_and_garbage() { + XCTAssertFalse(KoreanStockID.isKorean("")) + XCTAssertFalse(KoreanStockID.isKorean("kr")) + XCTAssertFalse(KoreanStockID.isKorean("kr_")) + } + + // MARK: - krIDToYahooSymbol + + func test_krIDToYahooSymbol_kospi() { + XCTAssertEqual(KoreanStockID.toYahooSymbol("kr_005930.ks"), "005930.KS") + } + + func test_krIDToYahooSymbol_kosdaq() { + XCTAssertEqual(KoreanStockID.toYahooSymbol("kr_293490.kq"), "293490.KQ") + } + + func test_krIDToYahooSymbol_handles_mixed_case_input() { + XCTAssertEqual(KoreanStockID.toYahooSymbol("kr_005930.KS"), "005930.KS") + XCTAssertEqual(KoreanStockID.toYahooSymbol("KR_005930.ks"), "005930.KS") + } + + func test_krIDToYahooSymbol_rejects_non_korean() { + XCTAssertNil(KoreanStockID.toYahooSymbol("sh600000")) + XCTAssertNil(KoreanStockID.toYahooSymbol("")) + } + + // MARK: - yahooSymbolToKrID + + func test_yahooSymbolToKrID_kospi() { + XCTAssertEqual(KoreanStockID.fromYahooSymbol("005930.KS"), "kr_005930.ks") + } + + func test_yahooSymbolToKrID_kosdaq() { + XCTAssertEqual(KoreanStockID.fromYahooSymbol("293490.KQ"), "kr_293490.kq") + } + + func test_yahooSymbolToKrID_handles_lowercase_input() { + XCTAssertEqual(KoreanStockID.fromYahooSymbol("005930.ks"), "kr_005930.ks") + } +}