2009-07-18 3 views
14

Я ищу простой способ для синтаксического анализа строки, которая содержит ISO-8601 продолжительность в Objective C. Результат должен быть чем-то полезным, например NSTimeInterval.Как разобрать длительность ISO-8601 в Objective C?

Пример продолжительности ISO-8601: P1DT13H24M17S, что означает 1 день, 13 часов, 24 минуты и 17 секунд.

ответ

6

Если вы точно знаете, какие поля вы будете получать, вы можете использовать один вызов 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 

Если какой-либо из полей может быть опущена, вы должны быть немного умнее в ваш синтаксический анализ, например:

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

Написание небольшого парсера было не совсем тем, что я имел в виду, когда начал искать легкое решение, но, похоже, нет другого пути. Я думал, что может быть трюк с использованием специальной строки формата с NSDateFormatter, но, похоже, она не работает с временными интервалами. Во всяком случае, я закончил тем, что написал аналогичный парсер. Спасибо всем за помощь. – Zargony

+2

Одна маленькая вещь, которую следует иметь в виду с приведенным выше примером кода (да, я понимаю, что это, вероятно, предназначалось для доказательства точки, а не для фактического потребления). Стандарт ISO 8601 позволяет задавать значения продолжительности как дробные значения (т. Е. 1,5, 0,6 и т. Д.), Которые могут быть разделены символом «.». или ",". –

0

Я просмотрел этот номер Wikipedia article, чтобы узнать, как работает ISO-8601. Я не эксперт по какао, но я уверен, что если вы сможете разобрать эту строку и извлечь компонент час, минуту, секунду, день и т. Д., То получить его в NSTimeInterval должно быть легко. Сложная часть разбирает его. Я бы, наверное, сделал бы это примерно так:

Сначала разделите строку на две отдельные строки: одну, представляющую дни, и одну, представляющую время. NSString имеет метод экземпляра componentsSeparatedByString:. NSString, который возвращает NSArray из подстрок исходного NSString отделенного параметра вы передаете в это будет выглядеть примерно так:

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

Далее, поиск первого элемента iso8601Parts для каждого от возможных индикаторов продолжительности дня (Y, M, W и D). Когда вы найдете его, возьмите все предыдущие цифры (и, возможно, десятичную точку), отбросьте их к плавающей точке и сохраните их где-нибудь. Помните, что если бы существовал только элемент времени, то iso8601Parts [0] будет пустой строкой.

Затем сделайте то же самое, что ищите временные части во втором элементе iso8601Parts для возможных индикаторов времени (H, M, S). Помните, что если бы существовал только компонент дня (т. Е. В исходной строке не было символа «T»), то iso8601Parts будет иметь длину только один, а попытка доступа к второму элементу вызовет исключение из-за пределов ,

NSTimeInterval - это просто длительное хранение нескольких секунд, поэтому конвертируйте отдельные фрагменты, которые вы вытащили за секунды, добавьте их вместе, сохраните их в своем NSTimeInterval и установите.

Извините, я знаю, что вы попросили «простой» способ сделать это, но, основываясь на моем (по общему признанию) поиске и знании API, это самый простой способ сделать это.

10

чистая 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

Это не подходит для P1M - (дает 1 минуту для этого, вместо этого он должен вернуться 1 месяц) –

7

Эта версия анализируйте каждую продолжительность работы без ошибок.
Внимание: Эта версия использует 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

Хорошая работа. Но вы должны упомянуть, что он должен запускаться в среде ARC. – Nekto

+0

Добавлено, спасибо за совет –

+0

Вы заработали секунды вместо минут. – orkenstein

0

Есть ответы уже, но я закончил реализацию еще одной версии, используя NSScanner. Эта версия игнорирует год и месяц, так как они не могут быть преобразованы в количество секунд.

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

Ниже приведен пример для Swift: (только в течение нескольких часов, минут и секунд)

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

Быстрые и грязные реализации

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

слегка модифицировать функции пользователя

Сергей Пекар

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

В настоящее время в Swift! (Да, он немного длинный, но он обрабатывает все случаи и единственное/множественное число).

Ручки лет, месяцев, недель, дней, часов, минут и секунд!

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 реализации: https://github.com/Igor-Palaguta/YoutubeEngine/blob/swift-2.3/YoutubeEngine/Classes/Parser/NSDateComponents+ISO8601.swift

Пример: let components = NSDateComponents(ISO8601String: "P1Y2M3DT4H5M6S")

Тесты: https://github.com/Igor-Palaguta/YoutubeEngine/blob/swift-2.3/Example/Tests/ISO8601DurationTests.swift

Он также корректно обрабатывает "P1M" и "PT1M" случаи

Swift3 реализация: https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Source/YoutubeEngine/Parser/NSDateComponents%2BISO8601.swift

Пример: let components = dateComponents(ISO8601String: "P1Y2M3DT4H5M6S")

Тесты: https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Tests/YoutubeEngineTests/ISO8601DurationTests.swift

Обновление 20.01.2017: Добавлена ​​поддержка

+0

Не включает поддержку недель. Например, P3W3DT20H31M21 не работает. –

+1

@DougSmith, спасибо за отзыв. Добавлена ​​поддержка недель, теперь вы можете использовать свойство weekOfYear, чтобы получить неделю –

+0

Awesome! спасибо –

1

Здесь недели скор 3 версия headkaze например: Этот формат был наиболее подходит в моем случае:

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) 

}