This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to do a natural sort on an NSArray?
Comparing version numbers
I have my app version saved as an NSString. The version is in the format x.y.z, or x.y (where x,y,z represent integers).
If I want to compare 2 versions (ie: 2 strings representing 2 different/same versions), what is the best way to go about doing this?
Thanks!
Sunny
i had the same problem and couldn't make it work. I finally decided to save version with NSNumber ...saved me a lot of time ...so i suggest you do the same
if you already have the app in the App Store...just do something like:
NSString *string = <get version>
if ([string isKindOfClass:[NSString class]]){
update version and save as NSNumber
}
using NSString to save version may seem like a good idea at first but its tricky when you try to update the app. Using NSNumber is easier because it ...well..uses numbers
so i looked through NSScanner Class Reference and came up with this solution:
int i1,i2,i3;
NSScanner *scanner =[NSScanner alloc]initWithString:string];
BOOL scanI1 = [scanner scanInteger:&i1];
[scanner setScanLocation:3];
BOOL scanI2 = [scanner scanInteger:&i2];
[scanner setScanLocation:5];
BOOL scanI3 = [scanner scanInteger:&i3];
[scanner release];
it's not pretty but it should work
There are about a million ways you could go about this, but here is a quick example:
void versionStringComponents(NSString* versionStr_, NSInteger* major__, NSInteger* minor__, NSInteger* bugfix__)
{
NSArray* elements = [versionStr_ componentsSeparatedByString:#"."];
*major__ = [[elements objectAtIndex:0] intValue];
*minor__ = [[elements objectAtIndex:1] intValue];
*bugfix__ = 0;
if([elements count] > 2)
{
*bugfix__ = [[elements objectAtIndex:2] intValue];
}
}
bool versionLessThan(NSString* versionStr1_, NSString* versionStr2_)
{
NSInteger major1 = 0, minor1 = 0, bugfix1 = 0;
versionStringComponents(versionStr1_, &major1, &minor1, &bugfix1);
NSInteger major2 = 0, minor2 = 0, bugfix2 = 0;
versionStringComponents(versionStr2_, &major2, &minor2, &bugfix2);
return (
major1 < major2 ||
(major1 == major2 && minor1 < minor2) ||
(major1 == major2 && minor1 == minor2 && bugfix1 < bugfix2)
);
}
Obviously this is a quick little hack, as versionStringComponents() just blindly separates the version numbers from a string, turns them into ints, etc. Also, it could use a few more comparison functions, but I'm sure you can figure those out.
This link, as mentioned by Saphrosit, is a much shorter way of accomplishing the same task, but requires the use of code from the Sparkle framework.
Related
I'm trying to repair some mis-numbered movie subtitle files (each sub is separated by a blank line). The following code scans up to the faulty subtitle index number in a test file. If I just 'printf' the faulty old indices and replacement new indices, everything appears just as expected.
//######################################################################
-(IBAction)scanToSubIndex:(id)sender
{
NSMutableString* tempString = [[NSMutableString alloc] initWithString:[theTextView string]];
int textLen = (int)[tempString length];
NSScanner *theScanner = [NSScanner scannerWithString:tempString];
while ([theScanner isAtEnd] == NO)
{
[theScanner scanUpToString:#"\r\n\r\n" intoString:NULL];
[theScanner scanString:#"\r\n\r\n" intoString:NULL];
if([theScanner scanLocation] >= textLen)
break;
else
{ // remove OLD subtitle index...
NSString *oldNumStr;
[theScanner scanUpToString:#"\r\n" intoString:&oldNumStr];
printf("old number:%s\n", [oldNumStr UTF8String]);
NSRange range = [tempString rangeOfString:oldNumStr];
[tempString deleteCharactersInRange:range];
// ...and insert SEQUENTIAL index
NSString *newNumStr = [self changeSubIndex];
printf("new number:%s\n\n", [newNumStr UTF8String]);
[tempString insertString:newNumStr atIndex:range.location];
}
}
printf("\ntempString\n\n:%s\n", [tempString UTF8String]);
}
//######################################################################
-(NSString*)changeSubIndex
{
static int newIndex = 1;
// convert int to string and return...
NSString *numString = [NSString stringWithFormat:#"%d", newIndex];
++newIndex;
return numString;
}
When I attempt to write the new indices to the mute string however, I end up with disordered results like this:
sub 1
sub 2
sub 3
sub 1
sub 5
sub 6
sub 7
sub 5
sub 9
sub 7
sub 8
An interesting observation (and possible clue?) is that when I reach subtitle number 1000, every number gets written to the mutable string in sequential order as required. I've been struggling with this for a couple of weeks now, and I can't find any other similar questions on SO. Any help much appreciated :-)
NSScanner & NSMutableString
NSMutableString is a subclass of NSString. In other words, you can pass NSMutableString at places where the NSString is expected. But it doesn't mean you're allowed to modify it.
scannerWithString: expects NSString. Translated to human language - I expect a string and I also do expect that the string is read-only (wont be modified).
In other words - your code is considered to be a programmer error - you give something to the NSScanner, NSScanner expects immutable string and you're modifying it.
We don't know what the NSScanner class is doing under the hood. There can be buffering or any other kind of optimization.
Even if you will be lucky with the mentioned scanLocation fix (in the comments), you shouldn't rely on it, because the under the hood implementation can change with any new release.
Don't do this. Not just here, but everywhere where you see immutable data type.
(There're situations where you can do it, but then you should really know what the under the hood implementation is doing, be certain that it wont be modified, etc. But generally speaking, it's not a good idea unless you know what you're doing.)
Sample
This sample code is based on the following assumptions:
we're talking about SubRip Text (SRT)
file is small (can easily fit memory)
rest of the SRT file is correct
especially the delimiter (#"\r\n")
#import Foundation;
NS_ASSUME_NONNULL_BEGIN
#interface SubRipText : NSObject
+ (NSString *)fixSubtitleIndexes:(NSString *)string;
#end
NS_ASSUME_NONNULL_END
#implementation SubRipText
+ (NSString *)fixSubtitleIndexes:(NSString *)string {
NSMutableString *result = [#"" mutableCopy];
__block BOOL nextLineIsIndex = YES;
__block NSUInteger index = 1;
[string enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) {
if (nextLineIsIndex) {
[result appendFormat:#"%lu\r\n", (unsigned long)index];
index++;
nextLineIsIndex = NO;
return;
}
[result appendFormat:#"%#\r\n", line];
nextLineIsIndex = line.length == 0;
}];
return result;
}
#end
Usage:
NSString *test = #"29\r\n"
"00:00:00,498 --> 00:00:02,827\r\n"
"Hallo\r\n"
"\r\n"
"4023\r\n"
"00:00:02,827 --> 00:00:06,383\r\n"
"This is two lines,\r\n"
"subtitles rocks!\r\n"
"\r\n"
"1234\r\n"
"00:00:06,383 --> 00:00:09,427\r\n"
"Maybe not,\r\n"
"just learn English :)\r\n";
NSString *result = [SubRipText fixSubtitleIndexes:test];
NSLog(#"%#", result);
Output:
1
00:00:00,498 --> 00:00:02,827
Hallo
2
00:00:02,827 --> 00:00:06,383
This is two lines,
subtitles rocks!
3
00:00:06,383 --> 00:00:09,427
Maybe not,
just learn English :)
There're other ways how to achieve this, but you should think about readability, speed of writing, speed of running, ... Depends on your usage - how many of them are you going to fix, etc.
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Comparing version numbers
How to use compare on a version number where theres less parts in one number in Objective-C?
I am attempting to sort an NSMutableArray of custom objects based on a property called referenceID which essentially resembles a version number.
It seems that treating referenceID as an NSNumber and sorting it using compareTo: almost gets it right, but where it breaks is cases such as:
Result: Should Be:
1.1.1 1.1.1
1.1.10 1.1.2
1.1.2 ...
... 1.1.9
1.1.9 1.1.10
(Where ... is 1.1.2 through 1.1.9)
Are there any built in functions that will sort this properly? Or should I get started writing the sorting algorithm?
If your reference id is a string, you can use localizedStandardCompare:, which compares numbers in strings according to their numerical value.
Example (with sortedArrayUsingComparator, because that is used by the OP in his comment):
NSArray *versions = #[#"2.1.1.1", #"2.10.1", #"2.2.1"];
NSArray *sorted = [versions sortedArrayUsingComparator:^NSComparisonResult(NSString *s1, NSString *s2) {
return [s1 localizedStandardCompare:s2];
}];
NSLog(#"%#", sorted);
Output:
2012-11-29 23:51:28.962 test27[1962:303] (
"2.1.1.1",
"2.2.1",
"2.10.1"
)
sort it with a block
#autoreleasepool {
//in this example, array of NSStrings
id array = #[#"1.1.1",#"2.2",#"1.0",#"1.1.0.1",#"1.1.2.0", #"1.0.3", #"2.1.1.1", #"2.1.1", #"2.1.10"];
//block
id sorted = [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSArray *comps1 = [obj1 componentsSeparatedByString:#"."];
NSArray *comps2 = [obj2 componentsSeparatedByString:#"."];
//get ints from comps
int res1 = 0;
for (int i=0; i<comps1.count; i++) {
res1 += [comps1[i] intValue] * (4 - i);
}
int res2 = 0;
for (int i=0; i<comps2.count; i++) {
res2 += [comps2[i] intValue] * (4 - i);
}
return res1<res2 ? NSOrderedAscending : res1>res2 ? NSOrderedSame : NSOrderedDescending;
}];
NSLog(#"%#", sorted);
}
I'm trying to use NSRange to hold a range of years, such as
NSRange years = NSMakeRange(2011, 5);
I know NSRange is used mostly for filtering, however I want to loop over the elements in the range. Is that possible without converting the NSRange into a NSArray?
It kind of sounds like you're expecting NSRange to be like a Python range object. It's not; NSRange is simply a struct
typedef struct _NSRange {
NSUInteger location;
NSUInteger length;
} NSRange;
not an object. Once you've created one, you can use its members in a plain old for loop:
NSUInteger year;
for(year = years.location; year < NSMaxRange(years); year++ ){
// Do your thing.
}
(Still working on the assumption that you're thinking about Python.) There's syntax in ObjC called fast enumeration for iterating over the contents of an NSArray that is pleasantly similar to a Python for loop, but since literal and primitive numbers can't be put into an NSArray, you can't go directly from an NSRange to a Cocoa array.
A category could make that easier, though:
#implementation NSArray (WSSRangeArray)
+ (id)WSSArrayWithNumbersInRange:(NSRange)range
{
NSMutableArray * arr = [NSMutableArray array];
NSUInteger i;
for( i = range.location; i < NSMaxRange(range); i++ ){
[arr addObject:[NSNumber numberWithUnsignedInteger:i]];
}
return arr;
}
Then you can create an array and use fast enumeration:
NSArray * years = [NSArray WSSArrayWithNumbersInRange:NSMakeRange(2011, 5)];
for( NSNumber * yearNum in years ){
NSUInteger year = [yearNum unsignedIntegerValue];
// and so on...
}
Remember that a NSRange is a structure holding two integers, representing the start and length of the range. You can easily loop over all of the contained integers using a for loop.
NSRange years = NSMakeRange(2011, 5);
NSUInteger year;
for(year = years.location; year < years.location + years.length; ++year) {
// Use the year variable here
}
This is a bit of an old question, but an alternative to using an NSArray would be to create an NSIndexSet with the desired range (using indexWithIndexesInRange: or initWithIndexesInRange:) and then using block enumeration as in https://stackoverflow.com/a/4209289/138772. (Seemed relevant as I was just checking on this myself.)
My alternate solution for this, was to define a macro just to make shorthand quicker.
#define NSRangeEnumerate(i, range) for(i = range.location; i < NSMaxRange(range); ++i)
To call it you do:
NSArray *array = #[]; // must contain at least the following range...
NSRange range = NSMakeRange(2011, 5);
NSUInteger i;
NSRangeEnumerate(i, range) {
id item = array[i];
// do your thing
}
personally I am still trying to figure out how I can write the macro so I can just call it like:
NSRangeEnumerate(NSUInteger i, range) {
}
which is not supported just yet... hope that helps or makes typing your program quicker
What's a simple implementation of the following NSString category method that returns the number of words in self, where words are separated by any number of consecutive spaces or newline characters? Also, the string will be less than 140 characters, so in this case, I prefer simplicity & readability at the sacrifice of a bit of performance.
#interface NSString (Additions)
- (NSUInteger)wordCount;
#end
I found the following solutions:
implementation of -[NSString wordCount]
implementation of -[NSString wordCount] - seems a bit simpler
But, isn't there a simpler way?
Why not just do the following?
- (NSUInteger)wordCount {
NSCharacterSet *separators = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSArray *words = [self componentsSeparatedByCharactersInSet:separators];
NSIndexSet *separatorIndexes = [words indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return [obj isEqualToString:#""];
}];
return [words count] - [separatorIndexes count];
}
I believe you have identified the 'simplest'. Nevertheless, to answer to your original question - "a simple implementation of the following NSString category...", and have it posted directly here for posterity:
#implementation NSString (GSBString)
- (NSUInteger)wordCount
{
__block int words = 0;
[self enumerateSubstringsInRange:NSMakeRange(0,self.length)
options:NSStringEnumerationByWords
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {words++;}];
return words;
}
#end
There are a number of simpler implementations, but they all have tradeoffs. For example, Cocoa (but not Cocoa Touch) has word-counting baked in:
- (NSUInteger)wordCount {
return [[NSSpellChecker sharedSpellChecker] countWordsInString:self language:nil];
}
It's also trivial to count words as accurately as the scanner simply using [[self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] count]. But I've found the performance of that method degrades a lot for longer strings.
So it depends on the tradeoffs you want to make. I've found the absolute fastest is just to go straight-up ICU. If you want simplest, using existing code is probably simpler than writing any code at all.
- (NSUInteger) wordCount
{
NSArray *words = [self componentsSeparatedByString:#" "];
return [words count];
}
Looks like the second link I gave in my question still reigns as not only the fastest but also, in hindsight, a relatively simple implementation of -[NSString wordCount].
A Objective-C one-liner version
NSInteger wordCount = word ? ([word stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet.invertedSet].length + 1) : 0;
Swift 3:
let words: [Any] = (string.components(separatedBy: " "))
let count = words.count
So I have an NSDictionary where the keys are years as NSString's and the value for each key is also an NSString which is sort of a description for the year. So for example, one key is "943 B.C.", another "1886". The problem I am encountering is that I want to sort them, naturally, in ascending order.
The thing is that the data source of these years is already in order, it's just that when I go ahead and call setValue:forKey the order is lost, naturally. I imagine figuring out a way to sort these NSString's might be a pain and instead I should look for a method of preserving the order at the insertion phase. What should I do? Should I instead make this an NSMutableArray in which every object is actually an NSDictionary consisting of the key being the year and the value being the description?
I guess I just answered my own question, but to avoid having wasted this time I'll leave this up in case anyone can recommend a better way of doing this.
Thanks!
EDIT: I went ahead with my own idea of NSMutableArray with NSDictionary entries to hold the key/value pairs. This is how I am accessing the information later on, hopefully I'm doing this correctly:
// parsedData is the NSMutableArray which holdes the NSDictionary entries
for (id entry in parsedData) {
NSString *year = [[entry allKeys] objectAtIndex:0];
NSString *text = [entry objectForKey:year];
NSLog(#"Year: %#, Text: %#", year, text);
}
Maintain a NSMutableArray to store the keys in order, in addition to the NSDictionary which holds all key-value pairs.
Here is a similar question.
You could either do it as an array of dictionaries, as you suggest, or as an array of strings where the strings are the keys to your original dictionary. The latter is probably a simpler way of going about it. NSDictionary does not, as I understand it, maintain any particular ordering of its keys, so attempting to sort the values there may be unwise.
I needed to solve a similar problem to sort strings of operating system names, such as "Ubuntu 10.04 (lucid)".
In my case, the string could have any value, so I sort by tokenizing and testing to see if a token is a number. I'm also accounting for a string like "8.04.2" being considered a number, so I have a nested level of tokenizing. Luckily, the nested loop is typically only one iteration.
This is from the upcoming OpenStack iPhone app.
- (NSComparisonResult)compare:(ComputeModel *)aComputeModel {
NSComparisonResult result = NSOrderedSame;
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
NSArray *tokensA = [self.name componentsSeparatedByString:#" "];
NSArray *tokensB = [aComputeModel.name componentsSeparatedByString:#" "];
for (int i = 0; (i < [tokensA count] || i < [tokensB count]) && result == NSOrderedSame; i++) {
NSString *tokenA = [tokensA objectAtIndex:i];
NSString *tokenB = [tokensB objectAtIndex:i];
// problem: 8.04.2 is not a number, so we need to tokenize again on .
NSArray *versionTokensA = [tokenA componentsSeparatedByString:#"."];
NSArray *versionTokensB = [tokenB componentsSeparatedByString:#"."];
for (int j = 0; (j < [versionTokensA count] || j < [versionTokensB count]) && result == NSOrderedSame; j++) {
NSString *versionTokenA = [versionTokensA objectAtIndex:j];
NSString *versionTokenB = [versionTokensB objectAtIndex:j];
NSNumber *numberA = [formatter numberFromString:versionTokenA];
NSNumber *numberB = [formatter numberFromString:versionTokenB];
if (numberA && numberB) {
result = [numberA compare:numberB];
} else {
result = [versionTokenA compare:versionTokenB];
}
}
}
[formatter release];
return result;
}