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
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,69 @@ struct Response {
}
```

#### Subscript Syntax Sugar

`AnyCodable` provides convenient subscript syntax for chained access to nested JSON structures:

```swift
let json = AnyCodable([
"users": [
["name": "Alice", "age": 25],
["name": "Bob", "age": 30]
],
"total": 2
])

// Chained access for nested structures
let firstName = json["users"][0]["name"].string // "Alice"
let secondAge = json["users"][1]["age"].int // 30
let total = json["total"].int // 2

// Safe access to non-existent paths, returns null instead of crashing
let invalid = json["users"][5]["name"].isNull // true
let missing = json["nonexistent"].isNull // true
```

#### Type Conversion Properties

`AnyCodable` provides various type conversion properties for quick value extraction:

| Property | Return Type | Description |
|----------|-------------|-------------|
| `.bool` | `Bool?` | Convert to boolean |
| `.int` | `Int?` | Convert to integer |
| `.uint` | `UInt?` | Convert to unsigned integer |
| `.double` | `Double?` | Convert to double |
| `.string` | `String?` | Convert to string |
| `.array` | `[Any]?` | Convert to array |
| `.dict` | `[String: Any]?` | Convert to dictionary |
| `.dictArray` | `[[String: Any]]?` | Convert to array of dictionaries |
| `.isNull` | `Bool` | Check if value is null |
| `.data` | `Data?` | Convert to JSON Data |

```swift
let json = AnyCodable([
"name": "phoenix",
"age": 33,
"isVIP": true,
"score": 99.5,
"tags": ["swift", "ios"],
"address": ["city": "Beijing", "country": "China"]
])

// Using type conversion properties
let name = json["name"].string // "phoenix"
let age = json["age"].int // 33
let isVIP = json["isVIP"].bool // true
let score = json["score"].double // 99.5
let tags = json["tags"].array // ["swift", "ios"]
let address = json["address"].dict // ["city": "Beijing", "country": "China"]

// Creating and checking null values
let nullValue = AnyCodable.null
print(nullValue.isNull) // true
```

### 19. Generate Default Instance

```swift
Expand Down
63 changes: 63 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,69 @@ struct Response {
}
```

#### 语法糖访问

`AnyCodable` 提供了便捷的下标语法糖,支持链式访问嵌套的 JSON 结构:

```swift
let json = AnyCodable([
"users": [
["name": "Alice", "age": 25],
["name": "Bob", "age": 30]
],
"total": 2
])

// 链式访问嵌套结构
let firstName = json["users"][0]["name"].string // "Alice"
let secondAge = json["users"][1]["age"].int // 30
let total = json["total"].int // 2

// 安全访问不存在的路径,返回 null 而不会崩溃
let invalid = json["users"][5]["name"].isNull // true
let missing = json["nonexistent"].isNull // true
```

#### 类型转换属性

`AnyCodable` 提供了多种类型转换属性,方便快速获取对应类型的值:

| 属性 | 返回类型 | 说明 |
|------|---------|------|
| `.bool` | `Bool?` | 转换为布尔值 |
| `.int` | `Int?` | 转换为整数 |
| `.uint` | `UInt?` | 转换为无符号整数 |
| `.double` | `Double?` | 转换为双精度浮点数 |
| `.string` | `String?` | 转换为字符串 |
| `.array` | `[Any]?` | 转换为数组 |
| `.dict` | `[String: Any]?` | 转换为字典 |
| `.dictArray` | `[[String: Any]]?` | 转换为字典数组 |
| `.isNull` | `Bool` | 检查是否为 null |
| `.data` | `Data?` | 转换为 JSON Data |

```swift
let json = AnyCodable([
"name": "phoenix",
"age": 33,
"isVIP": true,
"score": 99.5,
"tags": ["swift", "ios"],
"address": ["city": "Beijing", "country": "China"]
])

// 使用类型转换属性
let name = json["name"].string // "phoenix"
let age = json["age"].int // 33
let isVIP = json["isVIP"].bool // true
let score = json["score"].double // 99.5
let tags = json["tags"].array // ["swift", "ios"]
let address = json["address"].dict // ["city": "Beijing", "country": "China"]

// 创建和检查 null 值
let nullValue = AnyCodable.null
print(nullValue.isNull) // true
```

### 19. 生成默认实例

```swift
Expand Down
36 changes: 36 additions & 0 deletions Sources/ReerCodable/DateCodingStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,40 @@ public enum DateCodingStrategy {
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()

// MARK: - High Performance ISO8601 API (iOS 15+, macOS 12+)

/// Parse ISO8601 date string using modern Date.ISO8601FormatStyle (better performance)
/// Falls back to ISO8601DateFormatter on older OS versions
public static func parseISO8601(_ string: String) -> Date? {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) {
// Try standard ISO8601 format first (most common case, best performance)
if let date = try? Date(string, strategy: .iso8601) {
return date
}
// Try with fractional seconds
if let date = try? Date(
string,
strategy: Date.ISO8601FormatStyle(includingFractionalSeconds: true)
) {
return date
}
return nil
} else {
// Fallback for older OS versions
return iso8601Formatter.date(from: string)
?? iso8601FractionalSecondsFormatter.date(from: string)
}
}

/// Format date to ISO8601 string using modern Date.ISO8601FormatStyle (better performance)
/// Falls back to ISO8601DateFormatter on older OS versions
public static func formatISO8601(_ date: Date) -> String {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) {
return date.formatted(.iso8601)
} else {
// Fallback for older OS versions
return iso8601Formatter.string(from: date)
}
}
}
6 changes: 1 addition & 5 deletions Sources/ReerCodable/KeyedDecodingContainer+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,7 @@ extension KeyedDecodingContainer where K == AnyCodingKey {
}
case .iso8601:
return try decodeDateValue(type: type, keys: keys) { (rawValue: String) in
let date =
DateCodingStrategy.iso8601Formatter.date(from: rawValue)
?? DateCodingStrategy.iso8601FractionalSecondsFormatter.date(from: rawValue)

guard let date else {
guard let date = DateCodingStrategy.parseISO8601(rawValue) else {
throw ReerCodableError(text: "Decode date with iso8601 string failed for keys: \(keys)")
}
return date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ extension KeyedEncodingContainer where K == AnyCodingKey {
case .millisecondsSince1970:
return Int64(date.timeIntervalSince1970 * 1000)
case .iso8601:
return DateCodingStrategy.iso8601Formatter.string(from: date)
return DateCodingStrategy.formatISO8601(date)
case .formatted(let dateFormatter):
return dateFormatter.string(from: date)
}
Expand Down
119 changes: 119 additions & 0 deletions Tests/ReerCodableTests/DateCodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,122 @@ extension TestReerCodable {
#expect(dict.string("date7") == "2024Year12-Month10*Day 00h00m00s")
}
}

// MARK: - ISO8601 Fractional Seconds Tests

@Codable
struct ISO8601FractionalModel {
@DateCoding(.iso8601)
var standardDate: Date

@DateCoding(.iso8601)
var fractionalDate: Date

@DateCoding(.iso8601)
var highPrecisionDate: Date

@DateCoding(.iso8601)
var timezoneOffsetDate: Date

@DateCoding(.iso8601)
var fractionalWithTimezoneDate: Date
}

extension TestReerCodable {

/// Test ISO8601 parsing with various fractional seconds formats
@Test
func iso8601FractionalSeconds() throws {
let jsonData = """
{
"standardDate": "2024-06-15T10:30:00Z",
"fractionalDate": "2024-06-15T10:30:00.123Z",
"highPrecisionDate": "2024-06-15T10:30:00.123456Z",
"timezoneOffsetDate": "2024-06-15T18:30:00+08:00",
"fractionalWithTimezoneDate": "2024-06-15T18:30:00.500+08:00"
}
""".data(using: .utf8)!

// Decode
let model = try JSONDecoder().decode(ISO8601FractionalModel.self, from: jsonData)

// 2024-06-15T10:30:00Z = 1718447400.0
#expect(model.standardDate.timeIntervalSince1970 == 1718447400.0)

// 2024-06-15T10:30:00.123Z = 1718447400.123
#expect(abs(model.fractionalDate.timeIntervalSince1970 - 1718447400.123) < 0.001)

// 2024-06-15T10:30:00.123456Z - verify microseconds precision with modern API
#expect(abs(model.highPrecisionDate.timeIntervalSince1970 - 1718447400.123456) < 0.000001)

// 2024-06-15T18:30:00+08:00 = 2024-06-15T10:30:00Z = 1718447400.0
#expect(model.timezoneOffsetDate.timeIntervalSince1970 == 1718447400.0)

// 2024-06-15T18:30:00.500+08:00 = 2024-06-15T10:30:00.500Z = 1718447400.5
#expect(abs(model.fractionalWithTimezoneDate.timeIntervalSince1970 - 1718447400.5) < 0.001)

// Encode - verify output format is standard ISO8601 (without fractional seconds)
let encodedData = try JSONEncoder().encode(model)
let dict = encodedData.stringAnyDictionary

#expect(dict.string("standardDate") == "2024-06-15T10:30:00Z")
// Encoded dates should be in standard format (fractional seconds are not preserved in output)
#expect(dict.string("fractionalDate") == "2024-06-15T10:30:00Z")
#expect(dict.string("timezoneOffsetDate") == "2024-06-15T10:30:00Z")
}

/// Test DateCodingStrategy.parseISO8601 directly
@Test
func parseISO8601Directly() throws {
// Standard format
let date1 = DateCodingStrategy.parseISO8601("2024-01-15T12:00:00Z")
#expect(date1 != nil)
#expect(date1?.timeIntervalSince1970 == 1705320000.0)

// With milliseconds
let date2 = DateCodingStrategy.parseISO8601("2024-01-15T12:00:00.123Z")
#expect(date2 != nil)
#expect(abs(date2!.timeIntervalSince1970 - 1705320000.123) < 0.001)

// With microseconds - verify high precision support
let date3 = DateCodingStrategy.parseISO8601("2024-01-15T12:00:00.123456Z")
#expect(date3 != nil)
#expect(abs(date3!.timeIntervalSince1970 - 1705320000.123456) < 0.000001)

// With positive timezone offset
let date4 = DateCodingStrategy.parseISO8601("2024-01-15T20:00:00+08:00")
#expect(date4 != nil)
#expect(date4?.timeIntervalSince1970 == 1705320000.0) // Same as 12:00:00Z

// With negative timezone offset
let date5 = DateCodingStrategy.parseISO8601("2024-01-15T07:00:00-05:00")
#expect(date5 != nil)
#expect(date5?.timeIntervalSince1970 == 1705320000.0) // Same as 12:00:00Z

// Fractional with timezone offset (edge case: new API has bug, fallback to ISO8601DateFormatter)
let date6 = DateCodingStrategy.parseISO8601("2024-01-15T20:00:00.999+08:00")
#expect(date6 != nil)
#expect(abs(date6!.timeIntervalSince1970 - 1705320000.999) < 0.001)

// Invalid format should return nil
let invalidDate = DateCodingStrategy.parseISO8601("not-a-date")
#expect(invalidDate == nil)

let invalidDate2 = DateCodingStrategy.parseISO8601("2024/01/15")
#expect(invalidDate2 == nil)
}

/// Test DateCodingStrategy.formatISO8601 directly
@Test
func formatISO8601Directly() throws {
let date = Date(timeIntervalSince1970: 1705320000.0) // 2024-01-15T12:00:00Z
let formatted = DateCodingStrategy.formatISO8601(date)
#expect(formatted == "2024-01-15T12:00:00Z")

// Date with fractional seconds - output should still be standard format
let dateWithFraction = Date(timeIntervalSince1970: 1705320000.123)
let formattedFraction = DateCodingStrategy.formatISO8601(dateWithFraction)
// Note: formatISO8601 outputs standard format without fractional seconds
#expect(formattedFraction == "2024-01-15T12:00:00Z")
}
}
Loading