1/******************************************************************************
2 * $Id: NSStringAdditions.m 13510 2012-09-22 16:09:52Z livings124 $
3 *
4 * Copyright (c) 2005-2012 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#import "NSApplicationAdditions.h"
26#import "NSStringAdditions.h"
27
28#import "transmission.h"
29#import "utils.h"
30
31@interface NSString (Private)
32
33+ (NSString *) stringForFileSizeLion: (uint64_t) size showUnitUnless: (NSString *) notAllowedUnit unitsUsed: (NSString **) unitUsed;
34
35+ (NSString *) stringForSpeed: (CGFloat) speed kb: (NSString *) kb mb: (NSString *) mb gb: (NSString *) gb;
36
37@end
38
39@implementation NSString (NSStringAdditions)
40
41+ (NSString *) ellipsis
42{
43	return [NSString stringWithUTF8String: "\xE2\x80\xA6"];
44}
45
46- (NSString *) stringByAppendingEllipsis
47{
48	return [self stringByAppendingString: [NSString ellipsis]];
49}
50
51#warning use localizedStringWithFormat: directly when 10.8-only
52+ (NSString *) formattedUInteger: (NSUInteger) value
53{
54    if ([NSApp isOnMountainLionOrBetter])
55        return [NSString localizedStringWithFormat: @"%lu", value];
56    else
57    {
58        static NSNumberFormatter * numberFormatter;
59        static dispatch_once_t onceToken;
60        dispatch_once(&onceToken, ^{
61            numberFormatter = [[NSNumberFormatter alloc] init];
62            [numberFormatter setNumberStyle: NSNumberFormatterDecimalStyle];
63            [numberFormatter setMaximumFractionDigits: 0];
64        });
65        
66        return [numberFormatter stringFromNumber: [NSNumber numberWithUnsignedInteger: value]];
67    }
68}
69
70#warning should we take long long instead?
71+ (NSString *) stringForFileSize: (uint64_t) size
72{
73    if ([NSApp isOnMountainLionOrBetter])
74        return [NSByteCountFormatterMtLion stringFromByteCount: size countStyle: NSByteCountFormatterCountStyleFile];
75    else
76        return [self stringForFileSizeLion: size showUnitUnless: nil unitsUsed: nil];
77}
78
79#warning should we take long long instead?
80+ (NSString *) stringForFilePartialSize: (uint64_t) partialSize fullSize: (uint64_t) fullSize
81{
82    NSString * partialString, * fullString;
83    if ([NSApp isOnMountainLionOrBetter])
84    {
85        NSByteCountFormatter * fileSizeFormatter = [[NSByteCountFormatterMtLion alloc] init];
86        
87        fullString = [fileSizeFormatter stringFromByteCount: fullSize];
88        
89        //figure out the magniture of the two, since we can't rely on comparing the units because of localization and pluralization issues (for example, "1 byte of 2 bytes")
90        BOOL partialUnitsSame;
91        if (partialSize == 0)
92            partialUnitsSame = YES; //we want to just show "0" when we have no partial data, so always set to the same units
93        else
94        {
95            const unsigned int magnitudePartial = log(partialSize)/log(1000);
96            const unsigned int magnitudeFull = fullSize < 1000 ? 0 : log(fullSize)/log(1000); //we have to catch 0 with a special case, so might as well avoid the math for all of magnitude 0
97            partialUnitsSame = magnitudePartial == magnitudeFull;
98        }
99        
100        [fileSizeFormatter setIncludesUnit: !partialUnitsSame];
101        partialString = [fileSizeFormatter stringFromByteCount: partialSize];
102        
103        [fileSizeFormatter release];
104    }
105    else
106    {
107        NSString * units;
108        fullString = [self stringForFileSizeLion: fullSize showUnitUnless: nil unitsUsed: &units];
109        partialString = [self stringForFileSizeLion: partialSize showUnitUnless: units unitsUsed: nil];
110    }
111    
112    return [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "file size string"), partialString, fullString];
113}
114
115+ (NSString *) stringForSpeed: (CGFloat) speed
116{
117    return [self stringForSpeed: speed
118                kb: NSLocalizedString(@"KB/s", "Transfer speed (kilobytes per second)")
119                mb: NSLocalizedString(@"MB/s", "Transfer speed (megabytes per second)")
120                gb: NSLocalizedString(@"GB/s", "Transfer speed (gigabytes per second)")];
121}
122
123+ (NSString *) stringForSpeedAbbrev: (CGFloat) speed
124{
125    return [self stringForSpeed: speed kb: @"K" mb: @"M" gb: @"G"];
126}
127
128+ (NSString *) stringForRatio: (CGFloat) ratio
129{
130    //N/A is different than libtransmission's
131    if ((int)ratio == TR_RATIO_NA)
132        return NSLocalizedString(@"N/A", "No Ratio");
133    else if ((int)ratio == TR_RATIO_INF)
134        return [NSString stringWithUTF8String: "\xE2\x88\x9E"];
135    else
136    {
137        if (ratio < 10.0)
138            return [NSString localizedStringWithFormat: @"%.2f", tr_truncd(ratio, 2)];
139        else if (ratio < 100.0)
140            return [NSString localizedStringWithFormat: @"%.1f", tr_truncd(ratio, 1)];
141        else
142            return [NSString localizedStringWithFormat: @"%.0f", tr_truncd(ratio, 0)];
143    }
144}
145
146+ (NSString *) percentString: (CGFloat) progress longDecimals: (BOOL) longDecimals
147{
148    if (progress >= 1.0)
149        return [NSString localizedStringWithFormat: @"%d%%", 100];
150    else if (longDecimals)
151        return [NSString localizedStringWithFormat: @"%.2f%%", tr_truncd(progress * 100.0, 2)];
152    else
153        return [NSString localizedStringWithFormat: @"%.1f%%", tr_truncd(progress * 100.0, 1)];
154}
155
156+ (NSString *) timeString: (uint64_t) seconds showSeconds: (BOOL) showSeconds
157{
158    return [NSString timeString: seconds showSeconds: showSeconds maxFields: NSUIntegerMax];
159}
160
161+ (NSString *) timeString: (uint64_t) seconds showSeconds: (BOOL) showSeconds maxFields: (NSUInteger) max
162{
163    NSParameterAssert(max > 0);
164    
165    NSMutableArray * timeArray = [NSMutableArray arrayWithCapacity: MIN(max, 5)];
166    NSUInteger remaining = seconds; //causes problems for some users when it's a uint64_t
167    
168    if (seconds >= 31557600) //official amount of seconds in one year
169    {
170        const NSUInteger years = remaining / 31557600;
171        if (years == 1)
172            [timeArray addObject: NSLocalizedString(@"1 year", "time string")];
173        else
174            [timeArray addObject: [NSString stringWithFormat: NSLocalizedString(@"%u years", "time string"), years]];
175        remaining %= 31557600;
176        --max;
177    }
178    if (max > 0 && seconds >= (24 * 60 * 60))
179    {
180        const NSUInteger days = remaining / (24 * 60 * 60);
181        if (days == 1)
182            [timeArray addObject: NSLocalizedString(@"1 day", "time string")];
183        else
184            [timeArray addObject: [NSString stringWithFormat: NSLocalizedString(@"%u days", "time string"), days]];
185        remaining %= (24 * 60 * 60);
186        --max;
187    }
188    if (max > 0 && seconds >= (60 * 60))
189    {
190        [timeArray addObject: [NSString stringWithFormat: NSLocalizedString(@"%u hr", "time string"), remaining / (60 * 60)]];
191        remaining %= (60 * 60);
192        --max;
193    }
194    if (max > 0 && (!showSeconds || seconds >= 60))
195    {
196        [timeArray addObject: [NSString stringWithFormat: NSLocalizedString(@"%u min", "time string"), remaining / 60]];
197        remaining %= 60;
198        --max;
199    }
200    if (max > 0 && showSeconds)
201        [timeArray addObject: [NSString stringWithFormat: NSLocalizedString(@"%u sec", "time string"), remaining]];
202    
203    return [timeArray componentsJoinedByString: @" "];
204}
205
206- (NSComparisonResult) compareNumeric: (NSString *) string
207{
208    const NSStringCompareOptions comparisonOptions = NSNumericSearch | NSForcedOrderingSearch;
209    return [self compare: string options: comparisonOptions range: NSMakeRange(0, [self length]) locale: [NSLocale currentLocale]];
210}
211
212- (NSArray *) betterComponentsSeparatedByCharactersInSet: (NSCharacterSet *) separator
213{
214    NSMutableArray * components = [NSMutableArray array];
215    
216    NSUInteger i = 0;
217    while (i < [self length])
218    {
219        const NSRange range = [self rangeOfCharacterFromSet: separator options: 0 range: NSMakeRange(i, [self length]-i)];
220        
221        if (range.location == NSNotFound)
222        {
223            [components addObject: [self substringFromIndex: i]];
224            break;
225        }
226        else if (range.location != i)
227        {
228            const NSUInteger length = range.location - i;
229            [components addObject: [self substringWithRange: NSMakeRange(i, length)]];
230            
231            i += length;
232        }
233        
234        i += range.length;
235    }
236    
237    return components;
238}
239
240@end
241
242@implementation NSString (Private)
243
244+ (NSString *) stringForFileSizeLion: (uint64_t) size showUnitUnless: (NSString *) notAllowedUnit unitsUsed: (NSString **) unitUsed
245{
246    double convertedSize;
247    NSString * unit;
248    NSUInteger decimals;
249    if (size < pow(1000, 2))
250    {
251        convertedSize = size / 1000.0;
252        unit = NSLocalizedString(@"KB", "File size - kilobytes");
253        decimals = convertedSize >= 10.0 ? 0 : 1;
254    }
255    else if (size < pow(1000, 3))
256    {
257        convertedSize = size / powf(1000.0, 2);
258        unit = NSLocalizedString(@"MB", "File size - megabytes");
259        decimals = 1;
260    }
261    else if (size < pow(1000, 4))
262    {
263        convertedSize = size / powf(1000.0, 3);
264        unit = NSLocalizedString(@"GB", "File size - gigabytes");
265        decimals = 2;
266    }
267    else
268    {
269        convertedSize = size / powf(1000.0, 4);
270        unit = NSLocalizedString(@"TB", "File size - terabytes");
271        decimals = 2;
272    }
273    
274    //match Finder's behavior
275    NSNumberFormatter * numberFormatter = [[NSNumberFormatter alloc] init];
276    [numberFormatter setNumberStyle: NSNumberFormatterDecimalStyle];
277    [numberFormatter setMinimumFractionDigits: 0];
278    [numberFormatter setMaximumFractionDigits: decimals];
279    
280    NSString * fileSizeString = [numberFormatter stringFromNumber: [NSNumber numberWithFloat: convertedSize]];
281    [numberFormatter release];
282    
283    if (!notAllowedUnit || ![unit isEqualToString: notAllowedUnit])
284        fileSizeString = [fileSizeString stringByAppendingFormat: @" %@", unit];
285    
286    if (unitUsed)
287        *unitUsed = unit;
288    
289    return fileSizeString;
290}
291
292+ (NSString *) stringForSpeed: (CGFloat) speed kb: (NSString *) kb mb: (NSString *) mb gb: (NSString *) gb
293{
294    if (speed <= 999.95) //0.0 KB/s to 999.9 KB/s
295        return [NSString localizedStringWithFormat: @"%.1f %@", speed, kb];
296    
297    speed /= 1000.0;
298    
299    if (speed <= 99.995) //1.00 MB/s to 99.99 MB/s
300        return [NSString localizedStringWithFormat: @"%.2f %@", speed, mb];
301    else if (speed <= 999.95) //100.0 MB/s to 999.9 MB/s
302        return [NSString localizedStringWithFormat: @"%.1f %@", speed, mb];
303    else //insane speeds
304        return [NSString localizedStringWithFormat: @"%.2f %@", (speed / 1000.0), gb];
305}
306
307@end
308