Cómo analizar una duración ISO-8601 en Objective C?

Estoy buscando una manera fácil de analizar una cadena que contiene una duración ISO-8601 en Objective C. El resultado debe ser algo utilizable como NSTimeInterval .

Un ejemplo de una duración ISO-8601: P1DT13H24M17S , lo que significa 1 día, 13 horas, 24 minutos y 17 segundos.

Si sabe exactamente qué campos obtendrá, puede usar una invocación de sscanf() :

 const char *stringToParse = ...; int days, hours, minutes, seconds; NSTimeInterval interval; if(sscanf(stringToParse, "P%dDT%dH%dM%sS", &days, &hours, &minutes, &seconds) == 4) interval = ((days * 24 + hours) * 60 + minutes) * 60 + seconds; else ; // handle error, parsing failed 

Si se puede omitir alguno de los campos, deberá ser un poco más inteligente en su análisis sintáctico, por ejemplo:

 const char *stringToParse = ...; int days = 0, hours = 0, minutes = 0, seconds = 0; const char *ptr = stringToParse; while(*ptr) { if(*ptr == 'P' || *ptr == 'T') { ptr++; continue; } int value, charsRead; char type; if(sscanf(ptr, "%d%c%n", &value, &type, &charsRead) != 2) ; // handle parse error if(type == 'D') days = value; else if(type == 'H') hours = value; else if(type == 'M') minutes = value; else if(type == 'S') seconds = value; else ; // handle invalid type ptr += charsRead; } NSTimeInterval interval = ((days * 24 + hours) * 60 + minutes) * 60 + seconds; 

Una versión pura de Objective C …

 NSString *duration = @"P1DT10H15M49S"; int i = 0, days = 0, hours = 0, minutes = 0, seconds = 0; while(i < duration.length) { NSString *str = [duration substringWithRange:NSMakeRange(i, duration.length-i)]; i++; if([str hasPrefix:@"P"] || [str hasPrefix:@"T"]) continue; NSScanner *sc = [NSScanner scannerWithString:str]; int value = 0; if ([sc scanInt:&value]) { i += [sc scanLocation]-1; str = [duration substringWithRange:NSMakeRange(i, duration.length-i)]; i++; if([str hasPrefix:@"D"]) days = value; else if([str hasPrefix:@"H"]) hours = value; else if([str hasPrefix:@"M"]) minutes = value; else if([str hasPrefix:@"S"]) seconds = value; } } NSLog(@"%@", [NSString stringWithFormat:@"%d days, %d hours, %d mins, %d seconds", days, hours, minutes, seconds]); 

Esta versión analiza cada duración de Youtube sin errores.
Importante: esta versión usa ARC.

 - (NSString*)parseISO8601Time:(NSString*)duration { NSInteger hours = 0; NSInteger minutes = 0; NSInteger seconds = 0; //Get Time part from ISO 8601 formatted duration http://en.wikipedia.org/wiki/ISO_8601#Durations duration = [duration substringFromIndex:[duration rangeOfString:@"T"].location]; while ([duration length] > 1) { //only one letter remains after parsing duration = [duration substringFromIndex:1]; NSScanner *scanner = [[NSScanner alloc] initWithString:duration]; NSString *durationPart = [[NSString alloc] init]; [scanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] intoString:&durationPart]; NSRange rangeOfDurationPart = [duration rangeOfString:durationPart]; duration = [duration substringFromIndex:rangeOfDurationPart.location + rangeOfDurationPart.length]; if ([[duration substringToIndex:1] isEqualToString:@"H"]) { hours = [durationPart intValue]; } if ([[duration substringToIndex:1] isEqualToString:@"M"]) { minutes = [durationPart intValue]; } if ([[duration substringToIndex:1] isEqualToString:@"S"]) { seconds = [durationPart intValue]; } } return [NSString stringWithFormat:@"%02d:%02d:%02d", hours, minutes, seconds]; } 

Implementación de Swift2 : https://github.com/Igor-Palaguta/YoutubeEngine/blob/swift-2.3/YoutubeEngine/Classes/Parser/NSDateComponents+ISO8601.swift

Ejemplo: let components = NSDateComponents(ISO8601String: "P1Y2M3DT4H5M6S")

Pruebas: https://github.com/Igor-Palaguta/YoutubeEngine/blob/swift-2.3/Example/Tests/ISO8601DurationTests.swift

También maneja correctamente los casos “P1M” y “PT1M”

Implementación de Swift3 : https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Source/YoutubeEngine/Parser/NSDateComponents%2BISO8601.swift

Ejemplo: let components = dateComponents(ISO8601String: "P1Y2M3DT4H5M6S")

Pruebas: https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Tests/YoutubeEngineTests/ISO8601DurationTests.swift

Actualización 20.01.2017: Soporte agregado por semanas

función ligeramente modificante del usuario

Sergei Pekar

 + (NSString*)parseISO8601Time:(NSString*)duration { NSInteger hours = 0; NSInteger minutes = 0; NSInteger seconds = 0; //Get Time part from ISO 8601 formatted duration http://en.wikipedia.org/wiki/ISO_8601#Durations if ([duration rangeOfString:@"T"].location == NSNotFound || [duration rangeOfString:@"P"].location == NSNotFound) { NSLog(@"Time is not a part from ISO 8601 formatted duration"); return @"0:00 Error"; } duration = [duration substringFromIndex:[duration rangeOfString:@"T"].location]; while ([duration length] > 1) { //only one letter remains after parsing duration = [duration substringFromIndex:1]; NSScanner *scanner = [[NSScanner alloc] initWithString:duration]; NSString *durationPart = [[NSString alloc] init]; [scanner scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] intoString:&durationPart]; NSRange rangeOfDurationPart = [duration rangeOfString:durationPart]; if ((rangeOfDurationPart.location + rangeOfDurationPart.length) > duration.length) { NSLog(@"Time is not a part from ISO 8601 formatted duration"); return @"0:00 Error"; } duration = [duration substringFromIndex:rangeOfDurationPart.location + rangeOfDurationPart.length]; if ([[duration substringToIndex:1] isEqualToString:@"H"]) { hours = [durationPart intValue]; } if ([[duration substringToIndex:1] isEqualToString:@"M"]) { minutes = [durationPart intValue]; } if ([[duration substringToIndex:1] isEqualToString:@"S"]) { seconds = [durationPart intValue]; } } if (hours != 0) return [NSString stringWithFormat:@"%ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds]; else return [NSString stringWithFormat:@"%ld:%02ld", (long)minutes, (long)seconds]; } 

Aquí hay un ejemplo para swift: (solo por horas, minutos y segundos)

 func parseDuration(duration: String) -> Int { var days = 0 var hours = 0 var minutes = 0 var seconds = 0 var decisionMaker = 0 var factor = 1 let specifiers: [Character] = ["M", "H", "T", "P"] let length = count(duration) for i in 1...length { let index = advance(duration.startIndex, length - i) let char = duration[index] for specifier in specifiers { if char == specifier { decisionMaker++ factor = 1 } } if let value = String(char).toInt() { switch decisionMaker { case 0: seconds += value * factor factor *= 10 case 1: minutes += value * factor factor *= 10 case 2: hours += value * factor factor *= 10 case 4: days += value * factor factor *= 10 default: break } } } return seconds + (minutes * 60) + (hours * 3600) + (days * 3600 * 24) } 

Aquí está la rápida versión 3 de ejemplo de headkaze: Este formato fue el más adecuado en mi caso:

 private func parseISO8601Time(iso8601: String) -> String { let nsISO8601 = NSString(string: iso8601) var days = 0, hours = 0, minutes = 0, seconds = 0 var i = 0 while i < nsISO8601.length { var str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i)) i += 1 if str.hasPrefix("P") || str.hasPrefix("T") { continue } let scanner = Scanner(string: str) var value = 0 if scanner.scanInt(&value) { i += scanner.scanLocation - 1 str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i)) i += 1 if str.hasPrefix("D") { days = value } else if str.hasPrefix("H") { hours = value } else if str.hasPrefix("M") { minutes = value } else if str.hasPrefix("S") { seconds = value } } } if days > 0 { hours += 24 * days } if hours > 0 { return String(format: "%d:%02d:%02d", hours, minutes, seconds) } return String(format: "%d:%02d", minutes, seconds) } 

Busqué en este artículo de Wikipedia una referencia de cómo funciona realmente ISO-8601. No soy un experto en Cocoa, pero apuesto a que si puede analizar esa cadena y extraer el componente de hora, minuto, segundo, día, etc., conseguirlo en un NSTimeInterval debería ser fácil. La parte difícil es analizarla. Probablemente lo haría algo como esto:

Primero, divida la cadena en dos cadenas separadas: una que represente los días y otra que represente las horas. NSString tiene un método de instancia componentsSeparatedByString: NSString que devuelve un NSArray de subcadenas de su NSString original separadas por el parámetro que pasa. Se vería así:

 NSString* iso8601 = /*However you're getting your string in*/ NSArray* iso8601Parts = [iso8601 componentsSeparatedByString:@"T"]; 

A continuación, busque el primer elemento de iso8601Parts para cada uno de los posibles indicadores de duración del día (Y, M, W y D). Cuando encuentre uno, tome todos los dígitos precedentes (y posiblemente un punto decimal), colóquelos en un flotador y guárdelos en algún lugar. Recuerde que si solo había un elemento de tiempo, entonces iso8601Parts [0] será la cadena vacía.

Luego, haga lo mismo buscando partes de tiempo en el segundo elemento de iso8601Parts para posibles indicadores de tiempo (H, M, S). Recuerde que si solo hubiera un componente de día (es decir, no hubiera un carácter ‘T’ en la cadena original), entonces iso8601Parts solo será de longitud uno, y un bash de acceder al segundo elemento causará una excepción de fuera de límites. .

Un NSTimeInterval es solo una gran cantidad de segundos, así que convierta las piezas individuales que extrajo en segundos, agréguelas, guárdelas en su NSTimeInterval y estará listo.

Lo siento, sé que pediste una manera “fácil” de hacerlo, pero en función de mi (por cierto, ligera) búsqueda y el conocimiento de la API, esta es la forma más fácil de hacerlo.

Implementación rápida y sucia

  - (NSInteger)integerFromYoutubeDurationString:(NSString*)duration{ if(duration == nil){ return 0; } NSString *startConst = @"PT"; NSString *hoursConst = @"H"; NSString *minutesConst = @"M"; NSString *secondsConst = @"S"; NSString *hours = nil; NSString *minutes = nil; NSString *seconds = nil; NSInteger totalSeconds = 0; NSString *clean = [duration componentsSeparatedByString:startConst][1]; if([clean containsString:hoursConst]){ hours = [clean componentsSeparatedByString:hoursConst][0]; clean = [clean componentsSeparatedByString:hoursConst][1]; totalSeconds = [hours integerValue]*3600; } if([clean containsString:minutesConst]){ minutes = [clean componentsSeparatedByString:minutesConst][0]; clean = [clean componentsSeparatedByString:minutesConst][1]; totalSeconds = totalSeconds + [minutes integerValue]*60; } if([clean containsString:secondsConst]){ seconds = [clean componentsSeparatedByString:secondsConst][0]; totalSeconds = totalSeconds + [seconds integerValue]; } return totalSeconds; } 

Ya hay respuestas, pero terminé implementando otra versión usando NSScanner . Esta versión ignora año y mes ya que no se pueden convertir a la cantidad de segundos.

 static NSTimeInterval timeIntervalFromISO8601Duration(NSString *duration) { NSTimeInterval timeInterval = 0; NSScanner *scanner = [NSScanner scannerWithString:duration]; NSCharacterSet *designators = [NSCharacterSet characterSetWithCharactersInString:@"PYMWDTHMS"]; BOOL isScanningTime = NO; while (![scanner isAtEnd]) { double scannedNumber = 0; BOOL didScanNumber = [scanner scanDouble:&scannedNumber]; NSString *scanned = nil; if ([scanner scanCharactersFromSet:designators intoString:&scanned]) { if (didScanNumber) { switch ([scanned characterAtIndex:0]) { case 'D': timeInterval += scannedNumber * 60 * 60 * 24; break; case 'H': timeInterval += scannedNumber * 60 * 60; break; case 'M': if (isScanningTime) { timeInterval += scannedNumber * 60; } break; case 'S': timeInterval += scannedNumber; break; default: break; } } if ([scanned containsString:@"T"]) { isScanningTime = YES; } } } return timeInterval; } 

¡Ahora en Swift ! (Sí, es un poco largo, pero maneja todos los casos y singular / plural).

¡Maneja Años, Meses, Semanas, Días, Horas, Minutos y Segundos!

 func convertFromISO8601Duration(isoValue: AnyObject) -> String? { var displayedString: String? var hasHitTimeSection = false var isSingular = false if let isoString = isoValue as? String { displayedString = String() for val in isoString { if val == "P" { // Do nothing when parsing the 'P' continue }else if val == "T" { // Indicate that we are now dealing with the 'time section' of the ISO8601 duration, then carry on. hasHitTimeSection = true continue } var tempString = String() if val >= "0" && val < = "9" { // We need to know whether or not the value is singular ('1') or not ('11', '23'). if let safeDisplayedString = displayedString as String! where count(displayedString!) > 0 && val == "1" { let lastIndex = count(safeDisplayedString) - 1 let lastChar = safeDisplayedString[advance(safeDisplayedString.startIndex, lastIndex)] //test if the current last char in the displayed string is a space (" "). If it is then we will say it's singular until proven otherwise. if lastChar == " " { isSingular = true } else { isSingular = false } } else if val == "1" { // if we are just dealing with a '1' then we will say it's singular until proven otherwise. isSingular = true } else { // ...otherwise it's a plural duration. isSingular = false } tempString += "\(val)" displayedString! += tempString } else { // handle the duration type text. Make sure to use Months & Minutes correctly. switch val { case "Y", "y": if isSingular { tempString += " Year " } else { tempString += " Years " } break case "M", "m": if hasHitTimeSection { if isSingular { tempString += " Minute " } else { tempString += " Minutes " } } else { if isSingular { tempString += " Month " } else { tempString += " Months " } } break case "W", "w": if isSingular { tempString += " Week " } else { tempString += " Weeks " } break case "D", "d": if isSingular { tempString += " Day " } else { tempString += " Days " } break case "H", "h": if isSingular { tempString += " Hour " } else { tempString += " Hours " } break case "S", "s": if isSingular { tempString += " Second " } else { tempString += " Seconds " } break default: break } // reset our singular flag, since we're starting a new duration. isSingular = false displayedString! += tempString } } } return displayedString }