New: added the CPCache class

This commit is contained in:
william57m committed 2015-02-26 22:07:15 -05:00
1 parent 63f634af00
commit b40f980c62
4 files changed
+703

No files matched your search

+350
View File
@@ -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 <CPObject>
@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 <CPCacheDelegate> _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
+1
View File
@@ -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"
+14
View File
@@ -0,0 +1,14 @@
@import <Foundation/CPCache.j>
@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
+338
View File
@@ -0,0 +1,338 @@
@import <Foundation/CPCache.j>
/*
* CPCacheTest tests all methods from CPCache
*/
@implementation CPCacheTest : OJTestCase<CPCacheDelegate>
{
// 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