Adding a Counter to URL in Swift

April 2, 2019

I recently ran into a bug in a project when attempting to move a file:

“The File.eip” couldn’t be moved to “.archive” because an item with the same name already exists.

This little error propogated all the way to the top and terminating my Launch Agent1. Being a background task I couldn’t ask the user what they’d like to do about this error, so the method moving the file had to handle it.

The obvious solution is to add a file counter to the end, so File.eip becomes File 1.eip. I figured it would also make sense to make this incrementing method an extenion of URL, similar to .appendPathComponent(:)

Adding a Counter

The whole method looks like this:

func incrementingCounter(starting count: Int = 1, by increment: Int = 1, format: String = "%01d", delimiter: String = " ") -> URL {
    
    // Break apart the URL
    let ext = self.pathExtension
    var fileName = self.deletingPathExtension().lastPathComponent
    
    let tempDest = self.deletingLastPathComponent()
    var counter = count
    
    // Find any suffix digits
    fileCounter: if let digitRange = fileName.range(of: "\(delimiter)\\d+$", options: .regularExpression) {
        
        let delimiterSet = CharacterSet(charactersIn: delimiter)
        
        // Extract the digits
        let subString = String(fileName[digitRange]).trimmingCharacters(in: delimiterSet)
        guard let fileCounter = Int(subString) else { break fileCounter}
        
        // Increment the counter
        counter = fileCounter + increment
        
        // Remove the existing counter
        fileName.removeSubrange(digitRange)
        
    }
    
    // Append the counter with a space
    let formattedCounter = String(format: format, counter)
    let newName = fileName.appending("\(delimiter)\(formattedCounter)")
    return tempDest.appendingPathComponent(newName).appendingPathExtension(ext)
    
}

and comes in two flavors:

The first constructs a new URL and the second adds the counter in place. The parameters allow the user to customize how the incrementing takes place. The user can specify the inital number, how much to imcrement by, as well as the string format of the counter and the delimiter to seprate the counter from the file name.

Let’s look at what each part does.

To find the counter we first break the URL apart so we can deal with just the file name.

let ext = self.pathExtension
var fileName = self.deletingPathExtension().lastPathComponent

let tempDest = self.deletingLastPathComponent()

Once we have the file name we need to check if it already has a counter. To do this I’m using a small regular expression, "\(delimiter)\\d+$", which matches our delimiter followed by number. The $ anchors the expression to the end of the string so we don’t match files that contain numbers in their file names.

Specifying the delimiter provides for more flexibility. By default it’s a space, but that may not always be what we need. I regularly work with files that use _v\d{4} as a counter.

fileName.range(of: "\(delimiter)\\d+$", options: .regularExpression)

This method returns an optional range for the matching characters. If there is a range we attempt to convert the range into an integer.

let delimiterSet = CharacterSet(charactersIn: delimiter)

let subString = String(fileName[digitRange]).trimmingCharacters(in: delimiterSet)
guard let fileCounter = Int(subString) else { break fileCounter}

The break here is important: it uses a labeled break which lets us exit the current scope (in the case our if for the range). I’m using the labeled break to handle the case where a number can’t be made into an integer2.

In this case it will just append a new counter to the end of the file name.

Next we increment the counter, and importantly, remove the existing counter.

 // Increment the counter
counter = fileCounter + increment

// Remove the existing counter
fileName.removeSubrange(digitRange)

The last step in our method is to format the counter and append it to the file name.

// Append the counter with a space
let formattedCounter = String(format: format, counter)
let newName = fileName.appending(" \(formattedCounter)")
return tempDest.appendingPathComponent(newName).appendingPathExtension(ext)

The whole file is available over on GitHub


  1. Now I have tests that try to copy a duplicate file. [return]
  2. I haven’t figured out how to write a test for this [return]

Laydown Test with Victoria Lau

February 10, 2019

At the end of last year I partnered with stylist Victoria Lau to shoot another test, this time based around autumn and winter clothing.





Scripting Collections in Capture One 12

December 21, 2018

A few weeks ago Phase One released Capture One 12 with a slick new interface, support for plug-ins, and a fix for one of my pet bugs (I call it Ralph). There are also a handful of new properties that allow workflows to be automated.

With the release I’ve updated all of my scripts for Capture One 12.

User Property

Capture One collections include a new user property to help differentiate the session’s collections from a user’s favorites.

With this new property finding favorites is fairly simple:

tell front document of application "Capture One 12"
    set theFolder to captures
    set captureCollection to item 1 of (collections whose folder is theFolder and user is true)
end tell

This is also the key to making my capture folder navigation scripts work again.

Sorting

Another new feature is the sort order property, which allows a script to sort a collection by over a dozen different keys. The main use I’ve found for this is easy, automated batch renaming.

tell front document of application "Capture One 12"
    --  Sort by date
    set sorting order of current collection to by date
    set sorting reversed of current collection to false
end tell

Previously batch renaming was a multi-step manual process:

  1. Sort by the desired key
  2. Select all
  3. Reset the renaming counter
  4. Batch rename

All of these steps could be mapped to keyboard shortcuts, but who has time for that? A short script will do all of those steps in a single action. Paired with a few more lines of code, the script could move between all favorites and rename an entire session with in a single go.

Bonus: Progress Reporting

Anyone who has run a larger script that handles a lot of files has certainly run into the fact that Capture One blocks user input while a script is running. While this isn’t solved in 12, scripts can at least indicate they’re still running with the progress properties.

The progress is displayed in the same view as other Capture One progress bars.

tell application "Capture One 12"
    -- progress set up
    set progress total units to 10
    set progress completed units to 0
    set progress text to "Doing important things"

    -- for each task
    set progress completed units to progress completed units + 1
    set progress additional text to "Details about the important thing"
end tell

Impractical Practical Photography

September 20, 2018


There is a lot of doom and gloom in the photo industry surrounding renderings taking over photography. Some of them are old hat, like Ikea’s catalogues, some are new like virtual Instagram influencers, and each one heralds the end of our profession.

Apple, recently, has been going the other direction. The wallpapers for the latest iPhones and Apple Watches are all real photos and videos1. Planets made of soap, nebulas from ink and paint, and physics replacing a physics engine with giant versions of watch faces.

In both cases using CGI might have been easier, but Apple instead chose to allow their photographers to experiment and play with photography in ways that aren’t often found in commercial shoots. The results are visually stunning and, perhaps, even whimsical.


  1. The iPhone wallpapers were even shot on iPhones [return]

Church in the Rain

September 18, 2018