I\'m looking for an easy way to parse a string that contains an ISO-8601 duration in Objective C. The result should be something usable like a NSTimeI
Now in Swift
! (Yes it's a little long, but it handles all cases and singular/plural).
Handles Years, Months, Weeks, Days, Hours, Minutes, and Seconds!
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
}
I looked up this Wikipedia article for a reference to how ISO-8601 actually works. I'm no Cocoa expert, but I'm betting if you can parse that string and extract the component hour, minute, second, day, etc., getting it in to an NSTimeInterval should be easy. The tricky part is parsing it. I'd probably do it something like this:
First, split the string in to two separate strings: one representing the days, and one representing the times. NSString has an instance method componentsSeparatedByString:NSString that returns an NSArray of substrings of your original NSString separated by the parameter you pass in. It would look something like this:
NSString* iso8601 = /*However you're getting your string in*/
NSArray* iso8601Parts = [iso8601 componentsSeparatedByString:@"T"];
Next, search the first element of iso8601Parts for each of the possible day duration indicators (Y, M, W, and D). When you find one, grab all the preceeding digits (and possibly a decimal point), cast them to a float, and store them somewhere. Remember that if there was only a time element, then iso8601Parts[0] will be the empty string.
Then, do the same thing looking for time parts in the second element of iso8601Parts for possible time indicators (H, M, S). Remember that if there was only a day component (that is, there was no 'T' character in the original string), then iso8601Parts will only be of length one, and an attempt to access the second element will cause an out of bounds exception.
An NSTimeInterval is just a long storing a number of seconds, so convert the individual pieces you pulled out in to seconds, add them together, store them in your NSTimeInterval, and you're set.
Sorry, I know you asked for an "easy" way to do it, but based on my (admittedly light) searching around and knowledge of the API, this is the easiest way to do it.
Here is an example for swift: (only for hours, minutes and seconds)
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)
}
A pure Objective C version...
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]);
There are answers already, but I ended up implementing yet another version using NSScanner
. This version ignores year and month since they cannot be converted to number of seconds.
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;
}
If you know exactly which fields you'll be getting, you can use one invocation of 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
If any of the fields might be omitted, you'll need to be a little smarter in your parsing, e.g.:
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;