133 lines
4.4 KiB
Swift
133 lines
4.4 KiB
Swift
//
|
|
// ImageNamingUtils.swift
|
|
|
|
import CryptoKit
|
|
import Foundation
|
|
|
|
/// Utility for creating consistent image filenames across platforms
|
|
class ImageNamingUtils {
|
|
|
|
private static let imageExtension = ".jpg"
|
|
private static let hashLength = 12
|
|
|
|
/// Generates a deterministic filename for a problem image
|
|
static func generateImageFilename(problemId: String, timestamp: String, imageIndex: Int)
|
|
-> String
|
|
{
|
|
|
|
let input = "\(problemId)_\(timestamp)_\(imageIndex)"
|
|
let hash = createHash(from: input)
|
|
|
|
return "problem_\(hash)_\(imageIndex)\(imageExtension)"
|
|
}
|
|
|
|
/// Generates a deterministic filename using current timestamp
|
|
static func generateImageFilename(problemId: String, imageIndex: Int) -> String {
|
|
let timestamp = ISO8601DateFormatter().string(from: Date())
|
|
return generateImageFilename(
|
|
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
|
|
}
|
|
|
|
/// Extracts problem ID from an image filename
|
|
static func extractProblemIdFromFilename(_ filename: String) -> String? {
|
|
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
|
|
return nil
|
|
}
|
|
|
|
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
|
|
let parts = nameWithoutExtension.components(separatedBy: "_")
|
|
|
|
guard parts.count == 3 && parts[0] == "problem" else {
|
|
return nil
|
|
}
|
|
|
|
return parts[1]
|
|
}
|
|
|
|
/// Validates if a filename follows our naming convention
|
|
static func isValidImageFilename(_ filename: String) -> Bool {
|
|
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
|
|
return false
|
|
}
|
|
|
|
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
|
|
let parts = nameWithoutExtension.components(separatedBy: "_")
|
|
|
|
return parts.count == 3 && parts[0] == "problem" && parts[1].count == hashLength
|
|
&& Int(parts[2]) != nil
|
|
}
|
|
|
|
/// Migrates an existing filename to our naming convention
|
|
static func migrateFilename(oldFilename: String, problemId: String, imageIndex: Int) -> String {
|
|
|
|
if isValidImageFilename(oldFilename) {
|
|
return oldFilename
|
|
}
|
|
|
|
let timestamp = ISO8601DateFormatter().string(from: Date())
|
|
return generateImageFilename(
|
|
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
|
|
}
|
|
|
|
/// Creates a deterministic hash from input string
|
|
private static func createHash(from input: String) -> String {
|
|
let inputData = Data(input.utf8)
|
|
let hashed = SHA256.hash(data: inputData)
|
|
let hashString = hashed.compactMap { String(format: "%02x", $0) }.joined()
|
|
return String(hashString.prefix(hashLength))
|
|
}
|
|
|
|
/// Batch renames images for a problem to use our naming convention
|
|
static func batchRenameForProblem(problemId: String, existingFilenames: [String]) -> [String:
|
|
String]
|
|
{
|
|
var renameMap: [String: String] = [:]
|
|
|
|
for (index, oldFilename) in existingFilenames.enumerated() {
|
|
let newFilename = migrateFilename(
|
|
oldFilename: oldFilename, problemId: problemId, imageIndex: index)
|
|
if newFilename != oldFilename {
|
|
renameMap[oldFilename] = newFilename
|
|
}
|
|
}
|
|
|
|
return renameMap
|
|
}
|
|
|
|
/// Validates that a collection of filenames follow our naming convention
|
|
static func validateFilenames(_ filenames: [String]) -> ImageValidationResult {
|
|
var validImages: [String] = []
|
|
var invalidImages: [String] = []
|
|
|
|
for filename in filenames {
|
|
if isValidImageFilename(filename) {
|
|
validImages.append(filename)
|
|
} else {
|
|
invalidImages.append(filename)
|
|
}
|
|
}
|
|
|
|
return ImageValidationResult(
|
|
totalImages: filenames.count,
|
|
validImages: validImages,
|
|
invalidImages: invalidImages
|
|
)
|
|
}
|
|
}
|
|
|
|
// Result of image filename validation
|
|
struct ImageValidationResult {
|
|
let totalImages: Int
|
|
let validImages: [String]
|
|
let invalidImages: [String]
|
|
|
|
var isAllValid: Bool {
|
|
return invalidImages.isEmpty
|
|
}
|
|
|
|
var validPercentage: Double {
|
|
guard totalImages > 0 else { return 100.0 }
|
|
return (Double(validImages.count) / Double(totalImages)) * 100.0
|
|
}
|
|
}
|