Merge pull request #2321 from william57m/master

New: added the CPCache class
This commit is contained in:
Antoine Mercadal committed 2015-03-05 18:08:18 -08:00
commit 630aa53d98
4 files changed
+797

No files matched your search

+371
View File
@@ -0,0 +1,371 @@
/*
* 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"
@import "CPDictionary.j"
@import "CPString.j"
@class _CPCacheItem;
/*
* 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;
int _currentPosition;
BOOL _totalCostCache;
unsigned _implementedDelegateMethods;
CPString _name @accessors(property=name);
int _countLimit @accessors(property=countLimit);
int _totalCostLimit @accessors(property=totalCostLimit);
id <CPCacheDelegate> _delegate @accessors(property=delegate);
}
#pragma mark -
#pragma mark Initialization
/*!
Initializes the cache with default values
@return the initialized cache
*/
- (id)init
{
if (self = [super init])
{
_items = [[CPDictionary alloc] init];
_currentPosition = 0;
_totalCostCache = -1;
_implementedDelegateMethods = 0;
_name = @"";
_countLimit = 0;
_totalCostLimit = 0;
_delegate = nil;
}
return self;
}
#pragma mark -
#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];
// Invalid cost cache
_totalCostCache = -1;
// 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
[self _sendDelegateWillEvictObjectForKey:aKey];
// Remove object
[_items removeObjectForKey:aKey];
// Invalid cost cache
_totalCostCache = -1;
}
/*!
Removes all the objects from the cache.
*/
- (void)removeAllObjects
{
// Call delegate method to warn that the objects are going to be removed
var enumerator = [_items keyEnumerator],
key;
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 -
#pragma mark Setters
/*!
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];
}
/*!
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];
}
/*!
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 -
#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
{
if (_totalCostCache >= 0)
return _totalCostCache;
var enumerator = [_items objectEnumerator],
value;
_totalCostCache = 0;
while (value = [enumerator nextObject])
_totalCostCache += [value cost];
return _totalCostCache;
}
/*
* 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) {
return [[[_items objectForKey:k1] position] compare:[[_items objectForKey:k2] position]]; }];
// Affect new positions
for (var i = 0; i < sortedKeys.length; ++i)
[[_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)
*/
- (void)_cleanCache
{
// Check if the condition is satisfied
if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded])
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)
{
if (![self _isTotalCostLimitExceeded] && ![self _isCountLimitExceeded])
break;
// Call delegate method to warn that the object is going to be removed
[self _sendDelegateWillEvictObjectForKey:sortedKeys[i]];
// Remove object
[_items removeObjectForKey: sortedKeys[i]];
// 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
+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
+411
View File
@@ -0,0 +1,411 @@
@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];
// 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]];
}
- (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];
[cache setDelegate:self];
// Check result when add key in empty cache
[cache setObject:@"Object1" forKey:@"key1"]
[self assert:@"Object1" 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
{
// Setup cache
var cache = [[CPCache alloc] init];
[cache setDelegate:self];
[cache setObject:@"Object1" forKey:@"key1"];
[cache setObject:@"Object2" forKey:@"key2"];
// 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"]];
// 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)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)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)testSetDelegate
{
// Setup cache
var cache = [[CPCache alloc] init];
[cache setDelegate:self];
[self assert:self equals:[cache delegate]];
[self assert:(1 << 1) equals:cache._implementedDelegateMethods]
}
/*
* 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]];
// 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
{
// 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)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 setCountLimit: 2];
[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
{
// 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
{
// 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]];
}
@end