From b40f980c625ccf1ae139983201135ff631cf2d6a Mon Sep 17 00:00:00 2001 From: william57m Date: Thu, 26 Feb 2015 20:52:54 -0500 Subject: [PATCH 1/3] New: added the CPCache class --- Foundation/CPCache.j | 350 +++++++++++++++++++++++++++++ Foundation/Foundation.j | 1 + Tests/Foundation/CPCacheItemTest.j | 14 ++ Tests/Foundation/CPCacheTest.j | 338 ++++++++++++++++++++++++++++ 4 files changed, 703 insertions(+) create mode 100644 Foundation/CPCache.j create mode 100644 Tests/Foundation/CPCacheItemTest.j create mode 100644 Tests/Foundation/CPCacheTest.j diff --git a/Foundation/CPCache.j b/Foundation/CPCache.j new file mode 100644 index 000000000..b0e84890f --- /dev/null +++ b/Foundation/CPCache.j @@ -0,0 +1,350 @@ +/* + * CPCache.j + * Foundation + * + * Created by William Mura. + * Copyright 2015, William Mura. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +@import "CPObject.j" + +/* + * Class _CPCacheItem + * Represent an item of CPCache + * This class allow to associate a cost and a position to an object + * + * Attributes: + * - object: the stored object + * - cost: represent the cost (of memory) of the object + * - position: represent the insertion order to determine the oldest object + */ +@implementation _CPCacheItem : CPObject +{ + CPObject _object @accessors(property=object); + int _cost @accessors(property=cost); + int _position @accessors(property=position); +} + ++ (id)cacheItemWithObject:(CPObject)anObject cost:(int)aCost position:(int)aPosition +{ + var cacheItem = [[super alloc] init]; + + if (cacheItem) + { + cacheItem._object = anObject; + cacheItem._cost = aCost; + cacheItem._position = aPosition; + } + + return cacheItem; +} + +@end + + +/* + * Delegate CPCacheDelegate + * + * - cache:willEvictObject: is called when a object is going to be removed + * When the total cost or the count exceeds the total cost limit or the count limit + * And also when removeObjectForKey or removeAllObjects are called + */ +@protocol CPCacheDelegate + +@optional +- (void)cache:(CPCache)cache willEvictObject:(id)obj; + +@end + + +var CPCacheDelegate_cache_WillEvictObject = 1 << 1; + +/*! + @class CPCache + @ingroup foundation + @brief A collection-like container with discardable objects + + https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSCache_Class/index.html#//apple_ref/occ/instp/NSCache/delegate + + A CPCache object is a collection-like container, or cache, that stores key-value pairs, + similar to the CPDictionary class. Developers often incorporate caches to temporarily + store objects with transient data that are expensive to create. + + Reusing these objects can provide performance benefits, because their values do not have to be recalculated. + However, the objects are not critical to the application and can be discarded if memory is tight. + If discarded, their values will have to be recomputed again when needed. + */ +@implementation CPCache : CPObject +{ + CPDictionary _items @accessors(readonly); // TOFIX: delete accessors which is only needed for test + int _currentPosition; + int _countLimit; + int _totalCostLimit; + id _delegate; + + // Set of bit to determine which delegate has to respond + unsigned _implementedDelegateMethods; +} + + +#pragma mark Initialization + +/*! + Initializes the cache with default values + @return the initialized cache +*/ +- (id)init +{ + self = [super init]; + + if (self) + { + _items = [[CPDictionary alloc] init]; + _currentPosition = 0; + _countLimit = 0; + _totalCostLimit = 0; + _delegate = nil; + } + + return self; +} + + +#pragma mark Managing cache + +/*! + Returns the object which correspond to the given key + @param aKey the key for the object's entry + @return the object for the entry +*/ +- (id)objectForKey:(id)aKey +{ + return [[_items objectForKey:aKey] object]; +} + +/*! + Adds an object with default cost into the cache. + @param anObject the object to add in the cache + @param aKey the object's key +*/ +- (void)setObject:(id)anObject forKey:(id)aKey +{ + [self setObject:anObject forKey:aKey cost:0]; +} + +/*! + Adds an object with a cost into the cache. + @param anObject the object to add in the cache + @param aKey the object's key + @param aCost the object's cost +*/ +- (void)setObject:(id)anObject forKey:(id)aKey cost:(int)aCost +{ + // Check if the key already exist + if ([_items objectForKey:aKey]) + [self removeObjectForKey:aKey]; + + // Add object + [_items setObject:[_CPCacheItem cacheItemWithObject:anObject cost:aCost position:++_currentPosition] forKey:aKey]; + + // Clean cache to satisfy condition (< totalCostLimit & < countLimit) if necessary + [self _cleanCache]; +} + +/*! + Removes the object from the cache for the given key. + @param aKey the key of the object to be removed +*/ +- (void)removeObjectForKey:(id)aKey +{ + // Call delegate method to warn that the object is going to be removed + if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) + [_delegate cache:self willEvictObject:[[_items objectForKey:aKey] object]]; + + [_items removeObjectForKey:aKey]; +} + +/*! + Removes all the objects from the cache. +*/ +- (void)removeAllObjects +{ + _currentPosition = 0; + + // Call delegate method to warn that the objects are going to be removed + if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) + { + var enumerator = [_items keyEnumerator], + value; + + while (value = [enumerator nextObject]) + [_delegate cache:self willEvictObject:[[_items objectForKey:value] object]]; + } + + [_items removeAllObjects]; +} + + +#pragma mark Accessors + +/*! + Returns the count limit of the cache +*/ +- (int)countLimit +{ + return _countLimit; +} + +/*! + Sets the count limit of the cache. + Remove objects if not enough place to keep all of them + @param aCountLimit the new count limit +*/ +- (void)setCountLimit:(int)aCountLimit +{ + _countLimit = aCountLimit; + [self _cleanCache]; +} + +/*! + Returns the total cost limit of the cache +*/ +- (int)totalCostLimit +{ + return _totalCostLimit; +} + +/*! + Sets the total cost limit of the cache. + Remove objects if not enough place to keep all of them + @param aTotalCostLimit the new total cost limit +*/ +- (void)setTotalCostLimit:(int)aTotalCostLimit +{ + _totalCostLimit = aTotalCostLimit; + [self _cleanCache]; +} + +/*! + Returns the cache's delegate +*/ +- (id)delegate +{ + return _delegate; +} + +/*! + Sets the cache's delegate. + @param aDelegate the new delegate +*/ +- (void)setDelegate:(id)aDelegate +{ + if (_delegate === aDelegate) + return; + + _delegate = aDelegate; + _implementedDelegateMethods = 0; + + if ([_delegate respondsToSelector:@selector(cache:willEvictObject:)]) + _implementedDelegateMethods |= CPCacheDelegate_cache_WillEvictObject +} + + +#pragma mark Privates + +/* + * This method return the number of objects in the cache + */ +- (int)_count +{ + return [_items count]; +} + +/* + * This method return the total cost (addition of all object's cost in the cache) + */ +- (int)_totalCost +{ + var enumerator = [_items objectEnumerator], + cost = 0, + value; + + while (value = [enumerator nextObject]) + cost += [value cost]; + + return cost; +} + +/* + * This method resequence the position of objects + * Otherwise the position value could rise until to cause problem + */ +- (void)_resequencePosition +{ + _currentPosition = 1; + + // Sort keys by position + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction:function(k1, k2, context) + { + var o1 = [_items objectForKey:k1], + o2 = [_items objectForKey:k2]; + return ([o1 position] < [o2 position] ? CPOrderedAscending : ([o1 position] > [o2 position] ? CPOrderedDescending : CPOrderedSame)); + } context:nil]; + + // Affect new positions + for (var i = 0; i < sortedKeys.length; ++i) + [[_items objectForKey:sortedKeys[i]] setPosition:_currentPosition++]; +} + +/* + * This method clean the cache if the totalCost or the count exceeds the totalCostLimit or the countLimit + * until to satisfy conditions (totalCost < totalCostLimit and count < countLimit) + */ +- (void)_cleanCache +{ + // Check if the condition is satisfied (totalCost < totalCostLimit and count < countLimit) + if (([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0)) + { + // Sort keys by position + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction:function(k1, k2, context) + { + var o1 = [_items objectForKey:k1], + o2 = [_items objectForKey:k2]; + return ([o1 position] < [o2 position] ? CPOrderedAscending : ([o1 position] > [o2 position] ? CPOrderedDescending : CPOrderedSame)); + } context:nil]; + + // Remove oldest objects until to satisfy condition (totalCost < totalCostLimit and count < countLimit) + for (var i = 0; i < sortedKeys.length; ++i) + { + if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) + break; + + // Call delegate method to warn that the object is going to be removed + if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) + [_delegate cache:self willEvictObject:[[_items objectForKey:sortedKeys[i]] object]]; + + // Remove object + [_items removeObjectForKey: sortedKeys[i]]; + } + + // Resequence position of all objects + [self _resequencePosition]; + } +} + +@end \ No newline at end of file diff --git a/Foundation/Foundation.j b/Foundation/Foundation.j index 3fa485412..3fcf5481f 100755 --- a/Foundation/Foundation.j +++ b/Foundation/Foundation.j @@ -24,6 +24,7 @@ @import "CPArray.j" @import "CPBundle.j" @import "CPByteCountFormatter.j" +@import "CPCache.j" @import "CPCharacterSet.j" @import "CPCoder.j" @import "CPComparisonPredicate.j" diff --git a/Tests/Foundation/CPCacheItemTest.j b/Tests/Foundation/CPCacheItemTest.j new file mode 100644 index 000000000..89a982d10 --- /dev/null +++ b/Tests/Foundation/CPCacheItemTest.j @@ -0,0 +1,14 @@ +@import + + +@implementation CPCacheItemTest : OJTestCase + +- (void)testCacheItemWithObjectCostPosition +{ + var cacheItem = [_CPCacheItem cacheItemWithObject:"Hello Cappuccino!" cost:50 position:1]; + [self assert:[cacheItem object] equals:"Hello Cappuccino!"]; + [self assert:[cacheItem cost] equals:50]; + [self assert:[cacheItem position] equals:1]; +} + +@end diff --git a/Tests/Foundation/CPCacheTest.j b/Tests/Foundation/CPCacheTest.j new file mode 100644 index 000000000..57d325d91 --- /dev/null +++ b/Tests/Foundation/CPCacheTest.j @@ -0,0 +1,338 @@ +@import + + +/* + * CPCacheTest tests all methods from CPCache + */ +@implementation CPCacheTest : OJTestCase +{ + // Parameters to check delegate + int _countDelegateExecuted; + CPArray _caches; + CPArray _objects; +} + +- (void)setUp +{ + // Initialize data to check delegate + _countDelegateExecuted = 0; + _caches = [[CPArray alloc] init]; + _objects = [[CPArray alloc] init]; +} + + +/* + * Delegate method, called when an object is evicted + */ +- (void)cache:(CPCache)cache willEvictObject:(id)obj +{ + _countDelegateExecuted++; + [_caches addObject:cache]; + [_objects addObject:obj]; +} + + +/* + * Tests methods of CPCache + */ +- (void)testInit +{ + var cache = [[CPCache alloc] init]; + + [self assert:CPCache equals:[cache class]]; + [self assert:0 equals:[cache countLimit]]; + [self assert:0 equals:[cache totalCostLimit]]; + [self assert:nil equals:[cache delegate]]; +} + +- (void)testObjectForKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // With valid key + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + + // With invalid key + [self assert:nil equals:[cache objectForKey:@"key666"]]; + + // With nil key + [self assert:nil equals:[cache objectForKey:nil]]; +} + +- (void)testSetObjectForKeyDefault +{ + // Setup cache + var cache = [[CPCache alloc] init]; + + // Check result when add key in empty cache + [cache setObject:@"Object1" forKey:@"key1"] + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + + // Check result when add a key with existing key + [cache setObject:@"Object2" forKey:@"key1"] + [self assert:@"Object2" equals:[cache objectForKey:@"key1"]]; +} + +- (void)testSetObjectForKeyWithExistingKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Check result when add key in cache with total cost limit + [cache setObject:@"Object3" forKey:@"key1"]; + [self assert:@"Object3" equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testSetObjectForKeyDiscardByCost +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setTotalCostLimit:100]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:50]; + + // Check result when add key in cache with total cost limit + [cache setObject:@"Object3" forKey:@"key3" cost:1]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testSetObjectForKeyDiscardByCount +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setCountLimit:2]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Check result when add key in cache with count limit + [cache setObject:@"Object3" forKey:@"key3"]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testRemoveObjectForKey +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // Remove object + [cache removeObjectForKey:@"key1"]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; +} + +- (void)testRemoveAllObjects +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Remove all objects + [cache removeAllObjects]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:nil equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:2 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + [self assert:cache equals:[_caches objectAtIndex:1]]; + [self assert:@"Object2" equals:[_objects objectAtIndex:1]]; +} + + +/* + * Accessors and mutators + */ + +- (void)testCountLimit +{ + var cache = [[CPCache alloc] init]; + [self assert:0 equals:[cache countLimit]]; +} + +- (void)testSetCountLimit +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + [cache setObject:@"Object3" forKey:@"key3"]; + + // Set count limit + [cache setCountLimit:2]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + + // Check new count limit + [self assert:2 equals:[cache countLimit]]; +} + +- (void)testTotalCostLimit +{ + var cache = [[CPCache alloc] init]; + [self assert:0 equals:[cache totalCostLimit]]; +} + +- (void)testSetTotalCostLimit +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + // Set total cost limit + [cache setTotalCostLimit:100]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; + + // Check total cost limit + [self assert:100 equals:[cache totalCostLimit]]; +} + +- (void)testDelegate +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + + [self assert:self equals:[cache delegate]]; +} + +- (void)testSetDelegate +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; + + [self assert:self equals:[cache delegate]]; +} + +/* + * Test private methods + */ + +- (void)testCount +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + [self assert:3 equals:[cache _count]]; +} + +- (void)testTotalCost +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; + + [self assert:130 equals:[cache _totalCost]]; +} + +- (void)testResequencePosition +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; // 1 - 1 + [cache setObject:@"Object2" forKey:@"key2" cost:10]; + [cache setObject:@"Object3" forKey:@"key3" cost:70]; // 3 - 2 + [cache removeObjectForKey:@"key2"] + [cache setObject:@"Object4" forKey:@"key4" cost:70]; // 4 - 3 + + // Before resequence + [self assert:1 equals:[[[cache _items] objectForKey:@"key1"] position]]; + [self assert:3 equals:[[[cache _items] objectForKey:@"key3"] position]]; + [self assert:4 equals:[[[cache _items] objectForKey:@"key4"] position]]; + + [cache _resequencePosition]; + + // After resequence + [self assert:1 equals:[[[cache _items] objectForKey:@"key1"] position]]; + [self assert:2 equals:[[[cache _items] objectForKey:@"key3"] position]]; + [self assert:3 equals:[[[cache _items] objectForKey:@"key4"] position]]; +} + +- (void)testCleanCacheWithoutDiscarding +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setTotalCostLimit: 100] + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:40]; + + // Set total cost limit + [cache _cleanCache]; + [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:0 equals:_countDelegateExecuted]; +} + +- (void)testCleanCacheWithCostDiscarding +{ + // Not testable, but tested in testSetObjectForKeyDiscardByCost +} + +- (void)testCleanCacheWithCountDiscarding +{ + // Not testable, but tested in testSetObjectForKeyDiscardByCount +} + + +@end From d66de937d93308b30cc410add1aa1c0dd680e4e6 Mon Sep 17 00:00:00 2001 From: william57m Date: Sat, 28 Feb 2015 20:53:32 -0500 Subject: [PATCH 2/3] Formatting: to pass capp_lint and to respect Cappuccino's standards --- Foundation/CPCache.j | 231 +++++++++++++++++---------------- Tests/Foundation/CPCacheTest.j | 59 ++++----- 2 files changed, 146 insertions(+), 144 deletions(-) diff --git a/Foundation/CPCache.j b/Foundation/CPCache.j index b0e84890f..f24c56f3a 100644 --- a/Foundation/CPCache.j +++ b/Foundation/CPCache.j @@ -22,39 +22,10 @@ @import "CPObject.j" +@import "CPDictionary.j" +@import "CPString.j" -/* - * Class _CPCacheItem - * Represent an item of CPCache - * This class allow to associate a cost and a position to an object - * - * Attributes: - * - object: the stored object - * - cost: represent the cost (of memory) of the object - * - position: represent the insertion order to determine the oldest object - */ -@implementation _CPCacheItem : CPObject -{ - CPObject _object @accessors(property=object); - int _cost @accessors(property=cost); - int _position @accessors(property=position); -} - -+ (id)cacheItemWithObject:(CPObject)anObject cost:(int)aCost position:(int)aPosition -{ - var cacheItem = [[super alloc] init]; - - if (cacheItem) - { - cacheItem._object = anObject; - cacheItem._cost = aCost; - cacheItem._position = aPosition; - } - - return cacheItem; -} - -@end +@class _CPCacheItem; /* @@ -71,8 +42,8 @@ @end +var CPCacheDelegate_cache_willEvictObject_ = 1 << 1; -var CPCacheDelegate_cache_WillEvictObject = 1 << 1; /*! @class CPCache @@ -91,17 +62,19 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; */ @implementation CPCache : CPObject { - CPDictionary _items @accessors(readonly); // TOFIX: delete accessors which is only needed for test + CPDictionary _items; int _currentPosition; - int _countLimit; - int _totalCostLimit; - id _delegate; - - // Set of bit to determine which delegate has to respond + BOOL _totalCostCache; unsigned _implementedDelegateMethods; + + CPString _name @accessors(property=name); + int _countLimit @accessors(property=countLimit); + int _totalCostLimit @accessors(property=totalCostLimit); + id _delegate @accessors(property=delegate); } +#pragma mark - #pragma mark Initialization /*! @@ -110,12 +83,14 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; */ - (id)init { - self = [super init]; - - if (self) + if (self = [super init]) { _items = [[CPDictionary alloc] init]; _currentPosition = 0; + _totalCostCache = -1; + _implementedDelegateMethods = 0; + + _name = @""; _countLimit = 0; _totalCostLimit = 0; _delegate = nil; @@ -125,6 +100,7 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; } +#pragma mark - #pragma mark Managing cache /*! @@ -162,6 +138,9 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; // Add object [_items setObject:[_CPCacheItem cacheItemWithObject:anObject cost:aCost position:++_currentPosition] forKey:aKey]; + // Invalid cost cache + _totalCostCache = -1; + // Clean cache to satisfy condition (< totalCostLimit & < countLimit) if necessary [self _cleanCache]; } @@ -173,10 +152,13 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; - (void)removeObjectForKey:(id)aKey { // Call delegate method to warn that the object is going to be removed - if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) - [_delegate cache:self willEvictObject:[[_items objectForKey:aKey] object]]; + [self _sendDelegateWillEvictObjectForKey:aKey]; + // Remove object [_items removeObjectForKey:aKey]; + + // Invalid cost cache + _totalCostCache = -1; } /*! @@ -184,31 +166,24 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; */ - (void)removeAllObjects { - _currentPosition = 0; - // Call delegate method to warn that the objects are going to be removed - if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) - { - var enumerator = [_items keyEnumerator], - value; + var enumerator = [_items keyEnumerator], + key; - while (value = [enumerator nextObject]) - [_delegate cache:self willEvictObject:[[_items objectForKey:value] object]]; - } + while (key = [enumerator nextObject]) + [self _sendDelegateWillEvictObjectForKey:key] + // Remove all objects [_items removeAllObjects]; + + // Invalid cost cache and reset position counter + _totalCostCache = -1; + _currentPosition = 0; } -#pragma mark Accessors - -/*! - Returns the count limit of the cache -*/ -- (int)countLimit -{ - return _countLimit; -} +#pragma mark - +#pragma mark Setters /*! Sets the count limit of the cache. @@ -221,14 +196,6 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; [self _cleanCache]; } -/*! - Returns the total cost limit of the cache -*/ -- (int)totalCostLimit -{ - return _totalCostLimit; -} - /*! Sets the total cost limit of the cache. Remove objects if not enough place to keep all of them @@ -240,14 +207,6 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; [self _cleanCache]; } -/*! - Returns the cache's delegate -*/ -- (id)delegate -{ - return _delegate; -} - /*! Sets the cache's delegate. @param aDelegate the new delegate @@ -261,10 +220,11 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; _implementedDelegateMethods = 0; if ([_delegate respondsToSelector:@selector(cache:willEvictObject:)]) - _implementedDelegateMethods |= CPCacheDelegate_cache_WillEvictObject + _implementedDelegateMethods |= CPCacheDelegate_cache_willEvictObject_ } +#pragma mark - #pragma mark Privates /* @@ -280,14 +240,18 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; */ - (int)_totalCost { + if (_totalCostCache >= 0) + return _totalCostCache; + var enumerator = [_items objectEnumerator], - cost = 0, value; - while (value = [enumerator nextObject]) - cost += [value cost]; + _totalCostCache = 0; - return cost; + while (value = [enumerator nextObject]) + _totalCostCache += [value cost]; + + return _totalCostCache; } /* @@ -299,12 +263,9 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; _currentPosition = 1; // Sort keys by position - var sortedKeys = [[_items allKeys] sortedArrayUsingFunction:function(k1, k2, context) - { - var o1 = [_items objectForKey:k1], - o2 = [_items objectForKey:k2]; - return ([o1 position] < [o2 position] ? CPOrderedAscending : ([o1 position] > [o2 position] ? CPOrderedDescending : CPOrderedSame)); - } context:nil]; + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction: + function(k1, k2) { + return [[[_items objectForKey:k1] position] compare:[[_items objectForKey:k2] position]]; }]; // Affect new positions for (var i = 0; i < sortedKeys.length; ++i) @@ -313,38 +274,82 @@ var CPCacheDelegate_cache_WillEvictObject = 1 << 1; /* * This method clean the cache if the totalCost or the count exceeds the totalCostLimit or the countLimit - * until to satisfy conditions (totalCost < totalCostLimit and count < countLimit) + * until to satisfy condition (totalCost < totalCostLimit and count < countLimit) */ - (void)_cleanCache { - // Check if the condition is satisfied (totalCost < totalCostLimit and count < countLimit) - if (([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0)) + // Check if the condition is satisfied + if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) + return; + + // Sort keys by position + var sortedKeys = [[_items allKeys] sortedArrayUsingFunction: + function(k1, k2) { + return [[[_items objectForKey:k1] position] compare:[[_items objectForKey:k2] position]]; }]; + + // Remove oldest objects until to satisfy the break condition + for (var i = 0; i < sortedKeys.length; ++i) { - // Sort keys by position - var sortedKeys = [[_items allKeys] sortedArrayUsingFunction:function(k1, k2, context) - { - var o1 = [_items objectForKey:k1], - o2 = [_items objectForKey:k2]; - return ([o1 position] < [o2 position] ? CPOrderedAscending : ([o1 position] > [o2 position] ? CPOrderedDescending : CPOrderedSame)); - } context:nil]; + if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) + break; - // Remove oldest objects until to satisfy condition (totalCost < totalCostLimit and count < countLimit) - for (var i = 0; i < sortedKeys.length; ++i) - { - if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) - break; + // Call delegate method to warn that the object is going to be removed + [self _sendDelegateWillEvictObjectForKey:sortedKeys[i]]; - // Call delegate method to warn that the object is going to be removed - if (_implementedDelegateMethods & CPCacheDelegate_cache_WillEvictObject) - [_delegate cache:self willEvictObject:[[_items objectForKey:sortedKeys[i]] object]]; + // Remove object + [_items removeObjectForKey: sortedKeys[i]]; - // Remove object - [_items removeObjectForKey: sortedKeys[i]]; - } - - // Resequence position of all objects - [self _resequencePosition]; + // Invalid cost cache + _totalCostCache = -1; } + + // Resequence position of all objects + [self _resequencePosition]; +} + +@end + + +@implementation CPCache (CPCacheDelegate) + +- (void)_sendDelegateWillEvictObjectForKey:(id)aKey +{ + if (_implementedDelegateMethods & CPCacheDelegate_cache_willEvictObject_) + [_delegate cache:self willEvictObject:[[_items objectForKey:aKey] object]]; +} + +@end + + +/* + * Class _CPCacheItem + * Represent an item of CPCache + * This class allow to associate a cost and a position to an object + * + * Attributes: + * - object: the stored object + * - cost: represent the cost (of memory) of the object + * - position: represent the insertion order to determine the oldest object + */ +@implementation _CPCacheItem : CPObject +{ + CPObject _object @accessors(property=object); + int _cost @accessors(property=cost); + int _position @accessors(property=position); +} + ++ (id)cacheItemWithObject:(CPObject)anObject cost:(int)aCost position:(int)aPosition +{ + var cacheItem = [[self alloc] init]; + + if (cacheItem) + { + [cacheItem setObject:anObject]; + [cacheItem setCost:aCost]; + [cacheItem setPosition:aPosition]; + } + + return cacheItem; } @end \ No newline at end of file diff --git a/Tests/Foundation/CPCacheTest.j b/Tests/Foundation/CPCacheTest.j index 57d325d91..c16af881f 100644 --- a/Tests/Foundation/CPCacheTest.j +++ b/Tests/Foundation/CPCacheTest.j @@ -39,7 +39,15 @@ { var cache = [[CPCache alloc] init]; + // Private attributes [self assert:CPCache equals:[cache class]]; + [self assert:CPDictionary equals:[cache._items class]]; + [self assert:0 equals:cache._currentPosition]; + [self assert:-1 equals:cache._totalCostCache]; + [self assert:0 equals:cache._implementedDelegateMethods]; + + // Public attributes + [self assert:@"" equals:[cache name]]; [self assert:0 equals:[cache countLimit]]; [self assert:0 equals:[cache totalCostLimit]]; [self assert:nil equals:[cache delegate]]; @@ -65,14 +73,16 @@ { // Setup cache var cache = [[CPCache alloc] init]; + [cache setDelegate:self]; // Check result when add key in empty cache [cache setObject:@"Object1" forKey:@"key1"] [self assert:@"Object1" equals:[cache objectForKey:@"key1"]]; - // Check result when add a key with existing key - [cache setObject:@"Object2" forKey:@"key1"] - [self assert:@"Object2" equals:[cache objectForKey:@"key1"]]; + // Check delegate + [self assert:0 equals:_countDelegateExecuted]; + [self assert:0 equals:[_caches count]]; + [self assert:0 equals:[_objects count]]; } - (void)testSetObjectForKeyWithExistingKey @@ -83,7 +93,7 @@ [cache setObject:@"Object1" forKey:@"key1"]; [cache setObject:@"Object2" forKey:@"key2"]; - // Check result when add key in cache with total cost limit + // Check result when add object with existing key in cache [cache setObject:@"Object3" forKey:@"key1"]; [self assert:@"Object3" equals:[cache objectForKey:@"key1"]]; [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; @@ -179,12 +189,6 @@ * Accessors and mutators */ -- (void)testCountLimit -{ - var cache = [[CPCache alloc] init]; - [self assert:0 equals:[cache countLimit]]; -} - - (void)testSetCountLimit { // Setup cache @@ -209,12 +213,6 @@ [self assert:2 equals:[cache countLimit]]; } -- (void)testTotalCostLimit -{ - var cache = [[CPCache alloc] init]; - [self assert:0 equals:[cache totalCostLimit]]; -} - - (void)testSetTotalCostLimit { // Setup cache @@ -239,15 +237,6 @@ [self assert:100 equals:[cache totalCostLimit]]; } -- (void)testDelegate -{ - // Setup cache - var cache = [[CPCache alloc] init]; - [cache setDelegate:self]; - - [self assert:self equals:[cache delegate]]; -} - - (void)testSetDelegate { // Setup cache @@ -255,6 +244,7 @@ [cache setDelegate:self]; [self assert:self equals:[cache delegate]]; + [self assert:(1 << 1) equals:cache._implementedDelegateMethods] } /* @@ -281,6 +271,13 @@ [cache setObject:@"Object3" forKey:@"key3" cost:70]; [self assert:130 equals:[cache _totalCost]]; + + // Hack totalCost then invalidate it to be sure that it is recomputed + cache._totalCostCache = 650; + [self assert:650 equals:[cache _totalCost]]; + + cache._totalCostCache = -1; + [self assert:130 equals:[cache _totalCost]]; } - (void)testResequencePosition @@ -294,16 +291,16 @@ [cache setObject:@"Object4" forKey:@"key4" cost:70]; // 4 - 3 // Before resequence - [self assert:1 equals:[[[cache _items] objectForKey:@"key1"] position]]; - [self assert:3 equals:[[[cache _items] objectForKey:@"key3"] position]]; - [self assert:4 equals:[[[cache _items] objectForKey:@"key4"] position]]; + [self assert:1 equals:[[cache._items objectForKey:@"key1"] position]]; + [self assert:3 equals:[[cache._items objectForKey:@"key3"] position]]; + [self assert:4 equals:[[cache._items objectForKey:@"key4"] position]]; [cache _resequencePosition]; // After resequence - [self assert:1 equals:[[[cache _items] objectForKey:@"key1"] position]]; - [self assert:2 equals:[[[cache _items] objectForKey:@"key3"] position]]; - [self assert:3 equals:[[[cache _items] objectForKey:@"key4"] position]]; + [self assert:1 equals:[[cache._items objectForKey:@"key1"] position]]; + [self assert:2 equals:[[cache._items objectForKey:@"key3"] position]]; + [self assert:3 equals:[[cache._items objectForKey:@"key4"] position]]; } - (void)testCleanCacheWithoutDiscarding From 471fbee0902ed61106df33c9ec967beb6e232dfb Mon Sep 17 00:00:00 2001 From: william57m Date: Sun, 1 Mar 2015 13:05:22 -0500 Subject: [PATCH 3/3] Formatting: code readability and tests improved --- Foundation/CPCache.j | 20 ++++++++- Tests/Foundation/CPCacheTest.j | 82 ++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/Foundation/CPCache.j b/Foundation/CPCache.j index f24c56f3a..c8efe2e2d 100644 --- a/Foundation/CPCache.j +++ b/Foundation/CPCache.j @@ -272,6 +272,22 @@ var CPCacheDelegate_cache_willEvictObject_ = 1 << 1; [[_items objectForKey:sortedKeys[i]] setPosition:_currentPosition++]; } +/* + * Check if the totalCostLimit is exceeded + */ +- (BOOL)_isTotalCostLimitExceeded +{ + return ([self _totalCost] > _totalCostLimit && _totalCostLimit > 0); +} + +/* + * Check if the countLimit is exceeded + */ +- (BOOL)_isCountLimitExceeded +{ + return ([self _count] > _countLimit && _countLimit > 0); +} + /* * This method clean the cache if the totalCost or the count exceeds the totalCostLimit or the countLimit * until to satisfy condition (totalCost < totalCostLimit and count < countLimit) @@ -279,7 +295,7 @@ var CPCacheDelegate_cache_willEvictObject_ = 1 << 1; - (void)_cleanCache { // Check if the condition is satisfied - if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) + if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded]) return; // Sort keys by position @@ -290,7 +306,7 @@ var CPCacheDelegate_cache_willEvictObject_ = 1 << 1; // Remove oldest objects until to satisfy the break condition for (var i = 0; i < sortedKeys.length; ++i) { - if (!(([self _totalCost] > _totalCostLimit && _totalCostLimit > 0) || ([self _count] > _countLimit && _countLimit > 0))) + if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded]) break; // Call delegate method to warn that the object is going to be removed diff --git a/Tests/Foundation/CPCacheTest.j b/Tests/Foundation/CPCacheTest.j index c16af881f..ac50a0fc6 100644 --- a/Tests/Foundation/CPCacheTest.j +++ b/Tests/Foundation/CPCacheTest.j @@ -303,11 +303,49 @@ [self assert:3 equals:[[cache._items objectForKey:@"key4"] position]]; } +- (void)testIsTotalCostLimitExceeded +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + + // Not exceed (because of not limit) + [self assert:NO equals:[cache _isTotalCostLimitExceeded]]; + + // Not exceed (with limit) + [cache setTotalCostLimit:100]; + [self assert:NO equals:[cache _isTotalCostLimitExceeded]]; + + // Exceed (by hacking the cost of object) + [[cache._items objectForKey:@"key1"] setCost: 150]; + cache._totalCostCache = -1; + [self assert:YES equals:[cache _isTotalCostLimitExceeded]]; +} + +- (void)testIsCountLimitExceeded +{ + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setObject:@"Object1" forKey:@"key1"]; + + // Not exceed (because of not limit) + [self assert:NO equals:[cache _isCountLimitExceeded]]; + + // Not exceed (with limit) + [cache setCountLimit:1]; + [self assert:NO equals:[cache _isCountLimitExceeded]]; + + // Exceed (by hacking the items) + [cache._items setObject:[_CPCacheItem cacheItemWithObject:@"Object2" cost:0 position:3] forKey:@"key2"]; + [self assert:YES equals:[cache _isCountLimitExceeded]]; +} + - (void)testCleanCacheWithoutDiscarding { // Setup cache var cache = [[CPCache alloc] init]; - [cache setTotalCostLimit: 100] + [cache setTotalCostLimit: 100]; + [cache setCountLimit: 2]; [cache setDelegate:self]; [cache setObject:@"Object1" forKey:@"key1" cost:50]; [cache setObject:@"Object2" forKey:@"key2" cost:40]; @@ -323,12 +361,50 @@ - (void)testCleanCacheWithCostDiscarding { - // Not testable, but tested in testSetObjectForKeyDiscardByCost + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setTotalCostLimit: 100]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1" cost:50]; + [cache setObject:@"Object2" forKey:@"key2" cost:40]; + + // Hack cost of object + [[cache._items objectForKey:@"key2"] setCost:80]; + cache._totalCostCache = -1; + + // Set total cost limit + [cache _cleanCache]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; } - (void)testCleanCacheWithCountDiscarding { - // Not testable, but tested in testSetObjectForKeyDiscardByCount + // Setup cache + var cache = [[CPCache alloc] init]; + [cache setCountLimit:2]; + [cache setDelegate:self]; + [cache setObject:@"Object1" forKey:@"key1"]; + [cache setObject:@"Object2" forKey:@"key2"]; + + // Hack items + [cache._items setObject:[_CPCacheItem cacheItemWithObject:@"Object3" cost:0 position:3] forKey:@"key3"]; + + // Set total cost limit + [cache _cleanCache]; + [self assert:nil equals:[cache objectForKey:@"key1"]]; + [self assert:@"Object2" equals:[cache objectForKey:@"key2"]]; + [self assert:@"Object3" equals:[cache objectForKey:@"key3"]]; + + // Check delegate + [self assert:1 equals:_countDelegateExecuted]; + [self assert:cache equals:[_caches objectAtIndex:0]]; + [self assert:@"Object1" equals:[_objects objectAtIndex:0]]; }