crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

PUZToXDConverterTests.swift (4490B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 @Suite("PUZToXDConverter")
      7 struct PUZToXDConverterTests {
      8     @Test("Across Lite binary converts to XD and parses")
      9     func basicPuzzleConvertsAndParses() throws {
     10         let data = try puzData(
     11             width: 3,
     12             height: 3,
     13             solution: "ABCDEFGHI",
     14             title: "Mini PUZ",
     15             author: "Tester",
     16             copyright: "\u{00A9}2026",
     17             clues: [
     18                 "Across 1",
     19                 "Down 1",
     20                 "Down 2",
     21                 "Down 3",
     22                 "Across 4",
     23                 "Across 5"
     24             ]
     25         )
     26 
     27         let source = try PUZToXDConverter.convert(puzData: data)
     28         let xd = try XD.parse(source)
     29         let puzzle = Puzzle(xd: xd)
     30 
     31         #expect(puzzle.title == "Mini PUZ")
     32         #expect(puzzle.author == "Tester")
     33         #expect(xd.copyright == "\u{00A9}2026")
     34         #expect(puzzle.width == 3)
     35         #expect(puzzle.height == 3)
     36         #expect(puzzle.acrossClues.map(\.number) == [1, 4, 5])
     37         #expect(puzzle.downClues.map(\.number) == [1, 2, 3])
     38         #expect(xd.acrossClues.first?.answer == "ABC")
     39         #expect(xd.downClues.first?.answer == "ADG")
     40     }
     41 
     42     @Test("Blocks are translated to XD blocks")
     43     func blocksConvert() throws {
     44         let data = try puzData(
     45             width: 3,
     46             height: 3,
     47             solution: "ABC.D.EFG",
     48             title: "Blocks",
     49             author: "",
     50             copyright: "",
     51             clues: [
     52                 "Across 1",
     53                 "Down 2",
     54                 "Across 4"
     55             ]
     56         )
     57 
     58         let source = try PUZToXDConverter.convert(puzData: data)
     59         #expect(source.contains("ABC\n#D#\nEFG"))
     60 
     61         let puzzle = Puzzle(xd: try XD.parse(source))
     62         #expect(puzzle.cells[1][0].isBlock)
     63         #expect(puzzle.cells[1][2].isBlock)
     64     }
     65 
     66     @Test("All-uppercase PUZ title is displayed in title case")
     67     func uppercaseTitleIsTitleCased() throws {
     68         let data = try puzData(
     69             width: 3,
     70             height: 3,
     71             solution: "ABCDEFGHI",
     72             title: "THEMELESS MONDAY #875",
     73             author: "",
     74             copyright: "",
     75             clues: [
     76                 "Across 1",
     77                 "Down 1",
     78                 "Down 2",
     79                 "Down 3",
     80                 "Across 4",
     81                 "Across 5"
     82             ]
     83         )
     84 
     85         let puzzle = Puzzle(xd: try XD.parse(try PUZToXDConverter.convert(puzData: data)))
     86         #expect(puzzle.title == "Themeless Monday #875")
     87     }
     88 
     89     @Test("Mixed-case PUZ title is preserved")
     90     func mixedCaseTitleIsPreserved() throws {
     91         let data = try puzData(
     92             width: 3,
     93             height: 3,
     94             solution: "ABCDEFGHI",
     95             title: "Mini PUZ",
     96             author: "",
     97             copyright: "",
     98             clues: [
     99                 "Across 1",
    100                 "Down 1",
    101                 "Down 2",
    102                 "Down 3",
    103                 "Across 4",
    104                 "Across 5"
    105             ]
    106         )
    107 
    108         let puzzle = Puzzle(xd: try XD.parse(try PUZToXDConverter.convert(puzData: data)))
    109         #expect(puzzle.title == "Mini PUZ")
    110     }
    111 
    112     private func puzData(
    113         width: UInt8,
    114         height: UInt8,
    115         solution: String,
    116         title: String,
    117         author: String,
    118         copyright: String,
    119         clues: [String],
    120         notes: String = ""
    121     ) throws -> Data {
    122         let cellCount = Int(width) * Int(height)
    123         let solutionBytes = Array(solution.utf8)
    124         #expect(solutionBytes.count == cellCount)
    125 
    126         var data = Data(repeating: 0, count: 0x34)
    127         for (offset, byte) in "ACROSS&DOWN".utf8.enumerated() {
    128             data[0x02 + offset] = byte
    129         }
    130         data[0x18] = UInt8(ascii: "1")
    131         data[0x19] = UInt8(ascii: ".")
    132         data[0x1A] = UInt8(ascii: "3")
    133         data[0x2C] = width
    134         data[0x2D] = height
    135         data[0x2E] = UInt8(clues.count & 0xFF)
    136         data[0x2F] = UInt8((clues.count >> 8) & 0xFF)
    137 
    138         data.append(contentsOf: solutionBytes)
    139         data.append(contentsOf: Array(repeating: UInt8(ascii: "-"), count: cellCount))
    140 
    141         for string in [title, author, copyright] + clues + [notes] {
    142             data.append(try cp1252Data(string))
    143             data.append(0)
    144         }
    145         return data
    146     }
    147 
    148     private func cp1252Data(_ string: String) throws -> Data {
    149         try #require(string.data(using: .windowsCP1252))
    150     }
    151 }