2009-07-18 6 views
14

Sto cercando un modo semplice per analizzare una stringa che contiene una durata ISOISOnell'obiettivo C. Il risultato dovrebbe essere qualcosa di utilizzabile come uno NSTimeInterval.Come analizzare una durata ISO-8601 nell'obiettivo C?

Un esempio di durata ISO-8601: P1DT13H24M17S, che significa 1 giorno, 13 ore, 24 minuti e 17 secondi.

risposta

6

Se si sa esattamente quali campi sarai sempre, è possibile utilizzare un'invocazione 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 

Se uno qualsiasi dei campi potrebbero essere omessi, avrete bisogno di essere un po 'più intelligente in la tua analisi, ad esempio:

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

scrivere un parser piccolo non era esattamente quello che avevo in mente quando ho iniziato a cercare una soluzione facile, ma sembra che non ci sia davvero nessun altro modo. Ho pensato che potrebbe esserci un trucco usando una stringa di formato speciale con NSDateFormatter, ma non sembra funzionare con intervalli di tempo. Comunque, ho finito per scrivere un parser simile. Grazie a tutti per l'aiuto. – Zargony

+2

Una piccola cosa da tenere a mente con l'esempio di codice precedente (sì, mi rendo conto che probabilmente è stato pensato per dimostrare un punto e non per il consumo effettivo). Lo standard ISO 8601 consente di specificare i valori di durata come valori frazionari (cioè 1,5, 0,6, ecc.) Che possono essere delimitati da "." o ",". –

0

Ho cercato questo Wikipedia article per un riferimento a come funziona effettivamente ISO-8601. Non sono un esperto di Cocoa, ma sto scommettendo se puoi analizzare quella stringa ed estrarre il componente ora, minuto, secondo, giorno, ecc., Trovarlo in un NSTimeInterval dovrebbe essere facile. La parte difficile è analizzarlo. Probabilmente farei qualcosa del genere:

Prima dividere la stringa in due stringhe separate: una che rappresenta i giorni e una che rappresenta le volte. NSString ha un metodo di istanza componentsSeparatedByString:. NSString che restituisce un NSArray di sottostringhe del vostro NSString origine, separati dal parametro si passa Sarebbe simile a questa:

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

Avanti, cerca il primo elemento di iso8601Parts per ogni dei possibili indicatori di durata del giorno (Y, M, W e D). Quando ne trovi uno, prendi tutte le cifre precedenti (e possibilmente un punto decimale), gettale su un float e memorizzale da qualche parte. Ricorda che se ci fosse solo un elemento temporale, iso8601Parts [0] sarà la stringa vuota.

Quindi, fare la stessa cosa cercando parti temporali nel secondo elemento di iso8601Parts per possibili indicatori di tempo (H, M, S). Ricorda che se c'era solo un componente del giorno (cioè non c'era il carattere 'T' nella stringa originale), allora iso8601Parts sarà solo di lunghezza uno, e un tentativo di accedere al secondo elemento causerà un'eccezione fuori dai limiti .

Un NSTimeInterval è solo un lungo immagazzinamento di un numero di secondi, quindi converti i singoli pezzi estratti in secondi, aggiungili insieme, memorizzali nel tuo NSTimeInterval e sei pronto.

Scusa, so che hai chiesto un modo "facile" per farlo, ma sulla base della mia (notoriamente leggera) ricerca e conoscenza dell'API, questo è il modo più semplice per farlo.

10

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

Questo errore per P1M - (dà 1 minuto per questo invece dovrebbe restituire 1 mese) –

7

Questo Versi su parse ogni durata di youtube senza errori.
Importante: Questa versione utilizza 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

Buon lavoro. Ma devi dire che dovrebbe essere eseguito in ambiente ARC. – Nekto

+0

Aggiunto, grazie per i consigli –

+0

Hai sbagliato a digitare secondi anziché minuti. – orkenstein

0

ci sono risposte già, ma ho finito per attuare l'ennesima versione utilizzando NSScanner. Questa versione ignora anno e mese poiché non possono essere convertiti in numero di secondi.

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

Ecco un esempio di una rapida: (solo per ore, minuti e secondi)

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

rapido e sporco attuazione

- (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

modificando leggermente funzione dell'utente

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

Ora in Swift! (Sì, è un po 'lungo, ma gestisce tutti i casi e singolare/plurale).

Gestisce anni, mesi, settimane, giorni, ore, minuti e secondi!

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 di implementazione: https://github.com/Igor-Palaguta/YoutubeEngine/blob/swift-2.3/YoutubeEngine/Classes/Parser/NSDateComponents+ISO8601.swift

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

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

Inoltre gestisce correttamente casi "P1M" e "PT1M"

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

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

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

Aggiornamento 2017/01/20: Aggiunto il supporto per settimane

+0

Non include il supporto per settimane. P3W3DT20H31M21 fallirà ad esempio. –

+1

@DougSmith, grazie per il feedback. Aggiunto il supporto per settimane, ora puoi usare la proprietà weekOfYear per ottenere la settimana –

+0

Impressionante! Grazie –

1

Ecco rapida 3 versione di esempio headkaze: Questo formato è stato più adatto nel mio 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) 

}