//
// Solution.swift
// ChallengeBase
//
// Created by Radamés Vega-Alfaro on 2022-10-29.
//
import Foundation
enum ResourceExtensions : String {
case input = "in"
case output = "out"
}
public protocol Solution : Challenge {
// MARK: - Associated Types
associatedtype Input
associatedtype Output: Equatable
// MARK: - Properties
/// Collection of Test Cases to be performed for this Solution
var testCases: [TestCase] { get set }
/// Collection of DataSets to be used for testing against
var selectedResourceSets: [String] { get set }
/// Collection of Algorithms to be executed on top of our datasets
var selectedAlgorithms: [Algorithms] { get set }
// MARK: - Solution Methods
/// Receives Test Case data from input and (optional) output files and transforms them into TestCase objects (easier to act upon)
func assemble(_ rawInput: String, _ rawOutput: String?) -> (Input, Output?)
/// Performs a single Run
func activate(_ input: Input, algorithm: Algorithms) -> Output
// MARK: - Extension Methods
/// Executes all configured Test Cases and Algorithms (plus actual solve, if test cases are successful)
mutating func execute()
/// Outputs the solution to the given Algorithm
func solve(algorithm: Algorithms)
// MARK: - Helper Methods
mutating func setResourceSets(_ resources: [String])
mutating func setAlgorithms(_ algorithms: [Algorithms])
}
public extension Solution where Self: Challenge & Solution {
// MARK: - Computed Properties
var solution: String {
get { return String(describing: type(of: self)) }
}
// MARK: - Helper Methods
mutating func setResourceSets(_ resources: [String]) {
self.selectedResourceSets = resources
}
mutating func setAlgorithms(_ algorithms: [Algorithms]) {
self.selectedAlgorithms = algorithms
}
// MARK: - Assemble Methods
/// Logic to assemble all scenarios parting from the selected datasets, broken down one by one
mutating func assembleAll(algorithm: Algorithms) {
self.selectedResourceSets.forEach { dataset in
self.testCases.append(assembleSingle(dataset, algorithm)!)
}
}
mutating func assembleSingle(_ dataset: String, _ algorithm: Algorithms) -> TestCase? {
do {
// Read relevant Dataset
let inputData = try readDataSet(type: .input, named: dataset, algorithm: algorithm)
let outputData = try readDataSet(type: .output, named: dataset, algorithm: algorithm)
let assembled = assemble(inputData, outputData)
// Wrap assembled Dataset as a TestCase object
return TestCase(name: dataset, algorithm: algorithm, input: assembled.0, output: assembled.1)
} catch let error as NSError {
print("Something went wrong whilst attempting to assemble scenarios... \(error)")
return nil
}
}
internal func readDataSet(type: ResourceExtensions, named dataset: String? = nil, algorithm: Algorithms? = nil) throws -> String {
// Lets assume the following regarding the files being used...
//
// 1. File formats should be:
// a. for input file: `[.][.].in`
//
// b. for output file: `[.][.].out`
//
// 2. Whenever dataset and algorithm is not provided for input files,
// we fall back to using the default `.in` file
//
// Determine full file name
// File name parts
var resourceFile: [String] = [
self.solution,
(!(dataset?.trimmingCharacters(in: .whitespaces).isEmpty ?? true)) ? String(describing: dataset!) : "",
(algorithm != nil) ? String(describing: algorithm!) : "",
type.rawValue
]
do {
let fileSystem = FileManager.default
// Validate and read file
while (!fileSystem.fileExists(atPath: "\(self.baseResourcePath)/\(resourceFile.asResourceName)") && resourceFile.count > 1) {
// print("file >\(self.baseResourcePath)/\(resourceFile.asResourceName)< does not exist")
resourceFile.remove(at: resourceFile.count - 2)
}
// print("Reading file >\(self.baseResourcePath)/\(resourceFile.joined(separator: "."))<")
return try String(contentsOfFile: "\(self.baseResourcePath)/\(resourceFile.asResourceName)")
} catch let error as NSError {
print("Something went wrong while reading file \(resourceFile.asResourceName)! \(error)")
throw error
}
}
// MARK: - Activate Methods
mutating func activateAll() {
for (index, test) in self.testCases.enumerated() {
self.testCases[index].actualOutput = self.activate(test.input, algorithm: test.algorithm as! Self.Algorithms)
}
}
// MARK: - Assert Methods
func assertAll() -> Bool {
return self.testCases
.map { $0.expectedOutput == $0.actualOutput }
.allSatisfy { $0 }
}
mutating func execute() {
if selectedAlgorithms.count == 0 {
selectedAlgorithms = Array(Algorithms.allCases)
}
self.selectedAlgorithms.forEach { algorithm in
print("Running \(String(describing: self)) using algorithm \(algorithm)...")
self.testCases = []
// Step 1: Assemble
self.assembleAll(algorithm: algorithm)
// print(self.datasets)
// Step 2: Activate
self.activateAll()
// print(self.datasets)
// Step 3: Assert
let isSuccessfulTests = self.assertAll()
if isSuccessfulTests {
if self.testCases.count == 0 {
print("No Test Cases were provided! Executing real data...")
} else {
print("Test Cases executed successfully! Executing real data...")
}
self.solve(algorithm: algorithm)
} else {
print("Skipped solution execution! Test cases failed:")
self.testCases.forEach { dataset in
if !dataset.isSuccessful {
print("Using Algorithm \(dataset.algorithm), Test Case \(dataset.name)")
print("Expected ", terminator: "")
printSolution(for: dataset.expectedOutput!)
print()
print("Got ", terminator: "")
printSolution(for: dataset.actualOutput!)
}
}
}
print()
}
}
func solve(algorithm: Algorithms) {
do {
// Step 1: Assemble
let inputData = try readDataSet(type: .input)
let assembled = assemble(inputData, nil)
// Step 2: Activate
let result = self.activate(assembled.0, algorithm: algorithm)
// Step 3: Assert (show result)
print("> Using algorithm \(algorithm), output: ", terminator: "")
printSolution(for: result)
} catch let error as NSError {
print("Something went wrong with the actual scenario... \(error)")
}
}
func printSolution(for output: Output) {
if let array = output as? Array {
print()
for line in array {
print("\t\t \(line)")
}
} else {
print(output)
}
}
}