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