From b40f980c625ccf1ae139983201135ff631cf2d6a Mon Sep 17 00:00:00 2001 From: william57m Date: Thu, 26 Feb 2015 20:52:54 -0500 Subject: [PATCH] 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