andreiz / advent-of-code-2023-swift

Copilot and ChatGPT assisted Advent of Code 2023 to learn Swift.
0 stars 0 forks source link

Day 1: Trebuchet?! #1

Open andreiz opened 11 months ago

andreiz commented 11 months ago

https://adventofcode.com/2023/day/1

andreiz commented 11 months ago

I asked ChatGPT 4 to write the initial version with this prompt:

Write a Swift program that loops through every line in a text file, and for each line finds the first and the last digit in it, and then combines the digits into a number

I then also asked:

Add detailed comments before every line. Assume I am a Swift newbie.

This was the result:

// Import the Foundation framework. This framework provides essential data types,
// collections, and operating system services.
import Foundation 

// Define a function named 'findFirstAndLastDigit' which takes a string as input
// and returns an optional integer.
func findFirstAndLastDigit(in string: String) -> Int? {
    // Use the 'filter' method to iterate over each character in the string,
    // keeping only those that are numbers.
    let digits = string.filter { $0.isNumber } 

    // Use 'guard' to check if the first and last characters in the 'digits' string exist.
    // If they don't, the function will exit and return 'nil'.
    guard let first = digits.first, let last = digits.last else { return nil }

    // Combine the first and last digits into a new string, and then try to
    // convert this string to an integer. The result is returned. If the
    // conversion fails, it will return 'nil'.
    return Int(String(first) + String(last)) 
}

// Define a function named 'processFile' that takes a file path as a string and
// doesn't return anything.
func processFile(atPath path: String) {
    do {
        // Try to read the content of the file at the specified path as a string.
        // If this operation fails, the catch block will be executed.
        let content = try String(contentsOfFile: path, encoding: .utf8)

        // Split the content into an array of strings, each representing a line.
        // The splitting is based on newline characters.
        let lines = content.components(separatedBy: .newlines)

        // Iterate through each line in the 'lines' array.
        for line in lines {
            // Call 'findFirstAndLastDigit' for the current line.
            // If it finds digits, it will return an integer, otherwise nil.
            if let number = findFirstAndLastDigit(in: line) {
                // Print the line and the extracted number if digits are found.
                print("Line: \(line) -> Number: \(number)")
            } else {
                // Print a message indicating no digits were found in the line.
                print("Line: \(line) -> No digits found")
            }
        }
    } catch {
        // If there was an error in reading the file, print the error message.
        print("Error reading file: \(error)")
    }
}

// Define the file path as a string. Replace this with the actual path to your text file.
let filePath = "path/to/your/textfile.txt"

// Call the 'processFile' function with the specified file path to process the file.
processFile(atPath: filePath)
andreiz commented 11 months ago

I wasn't clear on the .newlines bit, so I asked ChatGPT to clarify. This was the answer:

In Swift, .newlines is a shorthand notation for accessing a static property of the CharacterSet class. When .newlines is used in a context where a CharacterSet is expected, Swift infers that it refers to CharacterSet.newlines.

This inference is based on the type context. Swift's type inference system allows you to write cleaner and more concise code by omitting the explicit type when it can be automatically inferred from the context.

In the code snippet content.components(separatedBy: .newlines), the components(separatedBy:) method expects an argument of type CharacterSet. Since .newlines is used in a parameter position where a CharacterSet is expected, Swift automatically infers that .newlines refers to CharacterSet.newlines.

andreiz commented 11 months ago

I added a couple of lines of code to keep track of the sum through the loop and then print it at the end.

Testing it on a sample 3 line file:

4three53pczsx1sevenmzmtrzz
four24qphdrxfsf
gdgj3f

The output was:

Line: 4three53pczsx1sevenmzmtrzz -> Number: 41
Line: four24qphdrxfsf -> Number: 24
Line: gdgj3f -> Number: 33
Line:  -> No digits found
Sum: 98

Apparently, If the input file ends with a newline character, the components(separatedBy: .newlines) method will interpret this as an extra, empty line. So we need to skip empty lines:

if line.isEmpty {
    continue
}
andreiz commented 11 months ago

Part 2 initial solution (just the updated function):

// Function to find the first and last digit (numeric or spelled-out) in a string
func findFirstAndLastDigit(in string: String) -> Int? {
    // Regular expression pattern to match numeric digits and spelled-out digits
    // The pattern looks for whole words that match either a single digit (\\d) or any of the spelled-out digits
    let pattern = "\\b(zero|one|two|three|four|five|six|seven|eight|nine|\\d)\\b"
    // Array to store all found digits
    var digits = [Int]()

    do {
        // Create a regular expression object from the pattern
        let regex = try NSRegularExpression(pattern: pattern)
        // Find matches in the input string
        // The matches method scans the string for matches to the pattern
        let matches = regex.matches(in: string, range: NSRange(string.startIndex..., in: string))

        // Process each match found in the string
        for match in matches {
            // Extract the range of each match
            let matchRange = Range(match.range, in: string)!
            // Extract the matched substring from the original string
            let matchString = String(string[matchRange])

            // Try to convert the matched string directly to a digit
            if let digit = Int(matchString) {
                digits.append(digit)
            } else if let digit = digitMap[matchString] {
                // If direct conversion fails, check if it's a spelled-out digit using the digitMap
                digits.append(digit)
            }
        }
    } catch {
        print("Error creating regex: \(error)")
        return nil
    }

    // Ensure that at least two digits were found
    guard let first = digits.first, let last = digits.last else { return nil }
    // Combine the first and last digits into a single integer
    return Int(String(first) + String(last))
}
andreiz commented 11 months ago

I see the regexp handling is not as concise as in some other languages.

The result of this was scored as too low by the advent checker. Probably because it doesn't handle cases where words overlap, like eighthree.

andreiz commented 11 months ago

Rewritten to use do 2 passes. First - extract numeric digits and their indices. Second - for each word in the digit map, iterate the input from start and locate all occurrences, adding to the array. Then sort by indices and pick first and last.

https://github.com/andreiz/advent-of-code-2023-swift/blob/7322903149e0a2e7f9696e6f9e877b9c9d9ab1f8/day-01/calibration-spelled.swift#L5-L54

andreiz commented 11 months ago

This passed