2009-07-18 14 views
14

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

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

Respuesta

6

Si usted sabe exactamente qué campos que voy a recibir, se puede utilizar 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 cualquiera de los campos podrían ser omitidos, que tendrá que ser un poco más inteligente en su análisis, 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; 
+0

escribir un pequeño analizador no era exactamente lo que tenía en mente cuando comencé a buscar una solución fácil, pero parece que de hecho no hay otra manera. Pensé que podría haber un truco usando una cadena de formato especial con NSDateFormatter, pero parece que no funciona con intervalos de tiempo. De todos modos, terminé escribiendo un analizador similar. Gracias a todos por ayudar. – Zargony

+2

Una cosa pequeña a tener en cuenta con el ejemplo de código anterior (sí, me doy cuenta de que probablemente fue para demostrar un punto y no para el consumo real). El estándar ISO 8601 permite que los valores de duración se especifiquen como valores fraccionarios (es decir, 1.5, 0.6, etc.) que pueden delimitarse mediante "." o ",". –

0

Busqué este Wikipedia article para ver cómo funciona realmente el 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 haga de la siguiente manera:

Primero, divida la cadena en dos cadenas separadas: una que represente los días y otra que represente los tiempos. NSString tiene un método de instancia componentsSeparatedByString:. NSString que devuelve un NSArray de subcadenas de su NSString fábrica, separados por el parámetro que se pasa en Sería algo parecido a esto:

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

A continuación, buscar el primer elemento de iso8601Parts para cada 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 intento de acceder al segundo elemento causará una excepción de fuera de límites. .

Un NSTimeInterval es solo un almacenamiento prolongado de varios segundos, de modo que convierta las piezas individuales que extrajo en segundos, agréguelas juntas, guárdelas en su NSTimeInterval, y listo.

Lo siento, sé que me pediste una forma "fácil" de hacerlo, pero en función de mi (evidentemente ligera) búsqueda y conocimiento de la API, esta es la forma más fácil de hacerlo.

10

Una versión Objective C pura ...

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]); 
+0

Esto falla para P1M - (da 1 minuto para esto, en su lugar, debe devolver 1 mes) –

7

Esta versi en analizar 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:@""] 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]; 
} 
+0

Buen trabajo. Pero debe mencionar que debe ejecutarse en un entorno ARC. – Nekto

+0

Agregado, gracias por su asesoramiento –

+0

Error de segundos en lugar de minutos. – orkenstein

0

hay respuestas ya, pero me terminó la implementación de una nueva versión utilizando 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; 
} 
1

Aquí se muestra un ejemplo para SWIFT: (sólo para 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) 
} 
0

de 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; 
} 
3

ligeramente modificar la función 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:@""] 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]; 
} 
0

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 
} 
5

Swift2 de ejecución: 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"

Swift3 aplicación: 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 01/20/2017: Se ha añadido soporte para la semana

+0

No incluye soporte durante semanas. P3W3DT20H31M21 fallará, por ejemplo. –

+1

@DougSmith, gracias por los comentarios. Se agregó soporte por semanas, ahora puede usar la propiedad weekOfYear para obtener la semana –

+0

¡Impresionante! Gracias –

1

Aquí es rápida 3 versión de ejemplo 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) 

} 
Cuestiones relacionadas