【问题标题】:How to parse an ISO-8601 duration in Objective C?如何在 Objective C 中解析 ISO-8601 持续时间?
【发布时间】:2010-11-11 21:32:09
【问题描述】:

我正在寻找一种简单的方法来解析在 Objective C 中包含 ISO-8601 duration 的字符串。结果应该是可用的,例如 NSTimeInterval

ISO-8601 持续时间示例:P1DT13H24M17S,表示 1 天 13 小时 24 分 17 秒。

【问题讨论】:

    标签: iphone objective-c datetime iso8601 nstimeinterval


    【解决方案1】:

    Swift3,4,5 实现: https://github.com/Igor-Palaguta/YoutubeEngine/blob/master/Source/YoutubeEngine/Parser/NSDateComponents%2BISO8601.swift

    示例: let components = try DateComponents(ISO8601String: "P1Y2M3DT4H5M6S")

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

    更新:修复了 DougSwith case "P3W3DT20H31M21"

    【讨论】:

    • 不包括数周的支持。例如,P3W3DT20H31M21 将失败。
    • @DougSmith,感谢您的反馈。添加了对周的支持,您现在可以使用 weekOfYear 属性来获取周
    • 太棒了!谢谢
    • 不支持毫秒。 P3W3DT20H31M21.5s 例如会失败
    【解决方案2】:

    纯 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]);
    

    【讨论】:

    • P1M 失败 - (为此提供 1 分钟而不是返回 1 个月)
    【解决方案3】:

    此版本解析每个 youtube 时长而不会出错。
    重要提示:此版本使用 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];
    }
    

    【讨论】:

    • 干得好。但不得不提的是,它应该是在ARC环境下运行的。
    • 你拼错了秒而不是分钟。
    【解决方案4】:

    如果您确切知道将获得哪些字段,则可以使用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;
    

    【讨论】:

    • 写一个小解析器并不是我开始寻找简单解决方案时的想法,但似乎确实没有其他办法。我认为使用带有 NSDateFormatter 的特殊格式字符串可能会有一个技巧,但它似乎不适用于时间间隔。无论如何,我最终写了一个类似的解析器。感谢大家的帮助。
    • 在上面的代码示例中要记住一件小事(是的,我意识到这可能是为了证明一个观点,而不是为了实际使用)。 ISO 8601 标准允许将持续时间值指定为可以用“.”分隔的小数值(即 1.5、0.6 等)。或“,”。
    【解决方案5】:

    稍微修改用户功能

    谢尔盖·佩卡尔

    + (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];
    }
    

    【讨论】:

      【解决方案6】:

      以下是 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)
      }
      

      【讨论】:

        【解决方案7】:

        这里是 swift 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)
        
        }
        

        【讨论】:

          【解决方案8】:

          我查阅了Wikipedia article 以了解 ISO-8601 的实际工作原理。我不是 Cocoa 专家,但我敢打赌,如果您可以解析该字符串并提取小时、分钟、秒、天等组件,那么将其放入 NSTimeInterval 应该很容易。棘手的部分是解析它。我可能会这样做:

          首先,将字符串拆分为两个单独的字符串:一个代表日期,一个代表时间。 NSString 有一个实例方法 componentsSeparatedByString:NSString ,它返回一个由你传入的参数分隔的原始 NSString 的子字符串的 NSArray。它看起来像这样:

          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 的长度仅为 1,并且尝试访问第二个元素将导致越界异常.

          一个 NSTimeInterval 只是一个很长的存储秒数,因此将您提取的单个片段转换为秒,将它们加在一起,将它们存储在您的 NSTimeInterval 中,然后就可以设置了。

          抱歉,我知道您要求用一种“简单”的方式来做这件事,但根据我(诚然是轻松的)搜索和对 API 的了解,这是最简单的方法。

          【讨论】:

            【解决方案9】:

            快速而肮脏的实现

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

            【讨论】:

              【解决方案10】:

              已经有了答案,但我最终使用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;
              }
              

              【讨论】:

                【解决方案11】:

                现在在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
                }
                

                【讨论】:

                  【解决方案12】:

                  Swift 4.2 版本

                  适用于年、月、日、小时、分钟、秒。 秒数可以是浮点数。

                  extension String{
                      public func parseISO8601Time() -> Duration {
                  
                          let nsISO8601 = NSString(string: self)
                  
                          var days = 0, hours = 0, minutes = 0, seconds: Float = 0, weeks = 0, months = 0, years = 0
                          var i = 0
                  
                          var beforeT:Bool = true
                  
                          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") {
                                  beforeT = !str.hasPrefix("T")
                                  continue
                              }
                  
                              let scanner = Scanner(string: str)
                              var value: Float = 0
                  
                              if scanner.scanFloat(&value) {
                  
                                  i += scanner.scanLocation - 1
                  
                                  str = nsISO8601.substring(with: NSRange(location: i, length: nsISO8601.length - i))
                  
                                  i += 1
                  
                                  if str.hasPrefix("Y") {
                                      years = Int(value)
                                  } else if str.hasPrefix("M") {
                                      if beforeT{
                                          months = Int(value)
                                      }else{
                                          minutes = Int(value)
                                      }
                                  } else if str.hasPrefix("W") {
                                      weeks = Int(value)
                                  } else if str.hasPrefix("D") {
                                      days = Int(value)
                                  } else if str.hasPrefix("H") {
                                      hours = Int(value)
                                  } else if str.hasPrefix("S") {
                                      seconds = value
                                  }
                              }
                          }
                  
                          return Duration(years: years, months: months, weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds)
                  }      
                  

                  持续时间结构:

                  public struct Duration {
                  
                  let daysInMonth: Int = 30
                  let daysInYear: Int = 365
                  
                  var years: Int
                  var months: Int
                  var weeks: Int
                  var days: Int
                  var hours: Int
                  var minutes: Int
                  var seconds: Float
                  
                  public func getMilliseconds() -> Int{
                      return Int(round(seconds*1000)) + minutes*60*1000 + hours*60*60*1000 + days*24*60*60*1000 + weeks*7*24*60*60*1000 + months*daysInMonth*24*60*60*1000 + years*daysInYear*24*60*60*1000
                  }
                  
                  public func getFormattedString() -> String{
                  
                      var formattedString = ""
                  
                      if years != 0{
                          formattedString.append("\(years)")
                          formattedString.append(" ")
                          formattedString.append(years == 1 ? "year".localized() : "years".localized())
                          formattedString.append(" ")
                      }
                  
                      if months != 0{
                          formattedString.append("\(months)")
                          formattedString.append(" ")
                          formattedString.append(months == 1 ? "month".localized() : "months".localized())
                          formattedString.append(" ")
                      }
                  
                      if weeks != 0{
                          formattedString.append("\(weeks)")
                          formattedString.append(" ")
                          formattedString.append(weeks == 1 ? "week".localized() : "weeks".localized())
                          formattedString.append(" ")
                      }
                  
                      if days != 0{
                          formattedString.append("\(days)")
                          formattedString.append(" ")
                          formattedString.append(days == 1 ? "day".localized() : "days".localized())
                          formattedString.append(" ")
                      }
                  
                      if seconds != 0{
                          formattedString.append(String(format: "%02d:%02d:%.02f", hours, minutes, seconds))
                      }else{
                          formattedString.append(String(format: "%02d:%02d", hours, minutes))
                      }
                  
                      return formattedString
                  }
                  

                  }

                  【讨论】:

                    猜你喜欢
                    • 2014-07-16
                    • 2014-08-16
                    • 1970-01-01
                    • 2021-05-21
                    • 2021-02-16
                    • 1970-01-01
                    • 2021-10-07
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多