Archived
1

Fundamental Redesign of the Model

Started to add Documentation
This commit is contained in:
Kim Wittenburg
2014-08-22 00:53:08 +02:00
parent 60760b8b3d
commit a37d587e1f
7 changed files with 486 additions and 55 deletions

View File

@@ -12,36 +12,281 @@
@class MPExpression, MPFunction, MPRangePath;
@protocol MPExpressionElement;
/*!
@class MPExpression
@brief An expression is the base object for any mathematical expression.
@discussion Every expression consists of string elements (represented by the
@c NSString class) and function (represented by the @c MPFunction
class) elements which both can be contained within an expression.
Functions in turn can have expressions as elements (also called
'children' in this context). Both expressions and functions are
mutable.
Through this organization expression are organized in a tree-like
structure (called the 'expression tree') allowing easy and
logical access to each element.
An expression can evaluate itself giving you either a
result or possibly an error if the expression was not constructed
correctly.
*/
@interface MPExpression : NSObject <NSCopying, NSCoding>
#pragma mark Creation Methods
- (instancetype)init; // Convenience
- (instancetype)initWithElement:(id<MPExpressionElement>)element; // Convenience
- (instancetype)initWithElements:(NSArray *)elements; // Designated Initializer
/*!
@method init
@brief Initlializes a newly created expression.
@discussion This method is a convenience initializer to initialize an empty
expression.
@return An expression.
*/
- (instancetype)init;
/*!
@method initWithElement:
@brief Initializes a newly created expression with one element.
@discussion This method is a convenience initializer to initialize an
expression with a single element.
@param element
The element to be added to the expression. The @c element will be
copied.
@return An expression initialized with @c element.
*/
- (instancetype)initWithElement:(id<MPExpressionElement>)element;
/*!
@method initWithElements:
@brief Initializes a newly created expression with the given elements.
@discussion This method is the designated initializer for the @c MPExpression
class.
@param elements
The elements that should be added to the expression. Each element
is copied and the copy is then added to the expression.
@return An expression containing the elements from @c elements.
*/
- (instancetype)initWithElements:(NSArray *)elements; /* designated initializer */
#pragma mark Working With the Expression Tree
@property (nonatomic, weak) MPFunction *parent; // Set automatically, nil for root expression
- (void)fixElements; // Called automatically, removes empty elements, joins subsequent strings
/*!
@property parent
@brief The receiver's parent.
@discussion Expressions are organized in a tree-like structure. Through this
property an expression's containing function can be accessed.
You should not set this property manually because that can cause
inconsistencies in the expression tree.
@return The parent of the receiver or @c nil if the receiver is the root
expression.
*/
@property (nonatomic, weak) MPFunction *parent;
/*!
@method fixElements
@brief Repairs any inconsistencies in the receiver.
@discussion This method goes over all elements in the receiver and tries to
repair inconsistencies that occured when mutating the receiver.
Since this method is called automatically everytime the receiver
is mutated there should be little need for you to call it
yourself.
*/
- (void)fixElements;
#pragma mark Primitive Methods
- (NSUInteger)length;
- (NSUInteger)numberOfElements;
- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)index;
- (NSArray *)elementsInRange:(NSRange)range;
- (NSUInteger)indexOfElement:(id<MPExpressionElement>)element;
- (void)replaceElementsInRange:(NSRange)range withElements:(NSArray *)elements;
// TODO: - (NSUInteger)indexOfElementAtLocation:(NSUInteger)location;
/*!
@method length
@brief Returns the length of the receiver.
@discussion The length of an expression is calculated by walking over each
element in the receiver and sending it a @c length message. This
method should be used to determine the number of digits or
symbols in an expression.
To address a symbol in the expression counted in the @c length
reference frame of an expression the word 'location' is used.
@return The length of the receiver. This is the number of symbols in all
elements in the receiver where a function element is counted as a
single symbol.
*/
- (NSUInteger)length;
/*!
@method numberOfElements
@brief Returns the number of elements in the receiver.
@discussion The number of elements may vary from the number of elements that
were added to the receiver (using either @c -initWithElements: or
@ -replaceElementsInRange:withElements:)
To address a specific symbol in an expression the word 'index' is
used. The index of elements is used when you access an
expression's elements using subscript syntax.
@return The current number of elements in the receiver.
*/
- (NSUInteger)numberOfElements;
/* Subscripting is supported for indexes and elements */
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx;
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
/*!
@method elementAtIndex:
@brief Returns the element at @c anIndex.
@discussion The element is not copied before it is returned. So be aware that
if you mutate a @c MPFunction object returned from this function
the changes will be reflected in the receiver.
This method can also be called using indexed subscript getter
syntax.
@param anIndex
The index of the element. If the index is greater than or equal
to the number of elements in the receiver an @c NSRangeException
is raised.
@return The element located at @c anIndex.
*/
- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)anIndex;
/*!
@method elementsInRange:
@brief Returns an array of the elements that are located in the
specified range. The range is specified in indexes.
@discussion The objects in the returned array are not copied before they are
returned. You should be aware of the fact that mutations to any
returned element will be reflected in the receiver.
If the @c range exceeds the receiver's bounds an @c
NSRangeException is raised.
@param range
The requested range within the receiver's bounds.
@return An array of objects that conform to the @c MPExpressionElement
protocol (that is @c NSString objects and @c MPFunction objects).
The length of the returned array is equal to the length of the
specified range.
*/
- (NSArray *)elementsInRange:(NSRange)range;
/*!
@method indexOfElement:
@brief Returns the index of @c element or @c NSNotFound if it was not
found.
@discussion
@param
@return
*/
#warning Implementation may be faulty
- (NSUInteger)indexOfElement:(id<MPExpressionElement>)element;
/*!
@method replaceSymbolsInRange:withElements:
@brief Replaces the elements in the given range with the contents of the
@c elements array.
@discussion This is the most primitive mutation method of @c MPExpression.
Every other mutating method utlimately must call this method.
After the receiver has been mutated @c -fixElements is called to
restore integrity of the receiver.
After the receiver has been mutated (and integrity has been
restored) the receiver sends a @c
-didChangeElementsInRangePath:replacementLength: to itself. For
more information see the documentation on that method.
@param range
The @c range is specified in the length reference
frame. Because of this this method can be directly used for user
interaction.
@param elements
The elements that should replace the symbols specified by @c
range.
*/
- (void)replaceSymbolsInRange:(NSRange)range
withElements:(NSArray *)elements;
// TODO: - (NSUInteger)indexOfElementAtSymbolLocation:(NSUInteger)location;
#warning Evaluating must possibly return error
- (double)doubleValue; // Evaluates Expression
#pragma mark Notifications
// All notification methods should create a new rangePath with the receiver's index added to the beginning of the path and then ascend the message to it's parent
// TODO: More notifications
/*!
@method didChangeElementsInRangePath:replacementLength:
@brief Called after the receiver has been mutated.
@discussion This method does nothing more than notify it's parent that it has
been mutated at the receiver's index. If you need to know about
changes in an expression you should override this method instead
of @c -replaceSymbolsInRange:withElements because this method
gives you information about the number of elements changed during
the mutation.
@param rangePath
The range path at which the receiver was changed starting at the
receiver.
@param replacementLength
The number of elements replacing the elements specified by @c
rangePath.
*/
- (void)didChangeElementsInRangePath:(MPRangePath *)rangePath
replacementLength:(NSUInteger)replacementLength;
#pragma mark Basic NSObject Methods
/*!
@method isEqualToExpression:
@brief Returns wether the receiver is equal to @c anExpression.
@param anExpression
The expression the receiver should be compared to.
@return @c YES if @c anExpression is equal to the receiver, @c NO
otherwise.
*/
- (BOOL)isEqualToExpression:(MPExpression *)anExpression;
- (NSString *)description;
@@ -49,21 +294,152 @@
@end
/* --------------------------------------------------------------------------- */
/* Extension Methods */
/* --------------------------------------------------------------------------- */
@interface MPExpression (MPExpressionExtension)
#pragma mark Working With the Expression Tree
- (id)elementAtIndexPath:(NSIndexPath *)indexPath; // Returns an MPExpression or id<MPExpressionElement>
/*!
@method elementAtIndexPath:
@brief Returns the element at the specified index path.
@discussion The returned object can be an @c NSString, a @c MPFunction or an
@c MPExpression depending on the element @c indexPath points to.
If any of the indexes exceed the bounds of the respective
receiver an @c NSRangeException is raised.
If the index path does not contain any indexes the receiver
itself is returned.
@param indexPath
The index path the required object is located at.
@return The element located at @c indexPath. The element is not copied
before it is returned. Be aware of the fact that any mutations
made to the returned object are reflected in the receiver.
*/
- (id)elementAtIndexPath:(NSIndexPath *)indexPath;
/*!
@method elementsInRangePath:
@brief Returns the elements in the specified range path.
@discussion If any of the indexes or the range exceed the bounds of the
respective receiver an @c NSRangeException is raised.
@param rangePath
The range path the requested objects are located at.
@return An array of objects specified by the range path. The returned
elements are not copied before they are returned. Be aware that
any mutations made to the returned objects are reflected in the
receiver.
*/
- (NSArray *)elementsInRangePath:(MPRangePath *)rangePath;
/*!
@method indexPath
@brief Returns the index path of the receiver in the expression tree.
@discussion The index path is calculated by walking up the expression tree
collecting the respective index of the receiver.
@return The index path of the receiver in the expression tree.
*/
- (NSIndexPath *)indexPath;
#pragma mark Working With Expressions
/*!
@method subexpressionFromLocation:
@brief Creates a new expression from the specified index (inclusive) to
the end of the receiver.
@discussion The elements in the newly created expression are copied to the
new expression. The location is specified in the length reference
frame.
If the given location exceeds the receiver's bounds a @c
NSRangeException is raised.
@param from
The first location to be included in the new expression.
@return A new expression.
*/
- (MPExpression *)subexpressionFromLocation:(NSUInteger)from;
/*!
@method subexpressionToLocation:
@brief Creates a new expression from the beginning to the specified
index (exclusive).
@discussion The elements in the newly created expression are copied to the
new expression. The location is specified in the length reference
frame.
If the given location exceeds the receiver's bounds a @c
NSRangeException is raised.
@param to
The first location not to be included in the new expression or
the length of the new expression.
@return A new expression.
*/
- (MPExpression *)subexpressionToLocation:(NSUInteger)to;
/*!
@method subexpressionWithRange:
@brief Creates a new expression with the symbols in the specified range.
@discussion The elements in the newly created expression are copied to the
new exoression. The range is specified in the length reference
frame.
If the given range exceeds the receiver's bounds a @c
NSRangeException is raised.
@param range
The range from which to create the new expression.
@return A new expression.
*/
- (MPExpression *)subexpressionWithRange:(NSRange)range;
#pragma mark Mutating Expressions
/*!
@method appendElement:
@brief Appends @c anElement to the receiver.
@param anElement
The element to append to the receiver.
*/
- (void)appendElement:(id<MPExpressionElement>)anElement;
/*!
@method appendElements:
@brief Appends the objects from @c elements to the receiver.
@param elements
The elements to append to the receiver.
*/
- (void)appendElements:(NSArray *)elements;
- (void)insertElement:(id<MPExpressionElement>)anElement atLocation:(NSUInteger)index;

View File

@@ -19,6 +19,9 @@
@interface MPExpression (MPExpressionPrivate)
- (NSUInteger)lengthOfElements:(NSArray *)elements;
- (NSUInteger)indexOfElementAtLocation:(NSUInteger)location;
- (void)validateElements:(NSArray *)elements;
- (BOOL)splitElementsAtLocation:(NSUInteger)location
insertionIndex:(out NSUInteger *)insertionIndex;
@@ -29,6 +32,22 @@
@implementation MPExpression (MPExpressionPrivate)
- (NSUInteger)lengthOfElements:(NSArray *)elements
{
NSUInteger length = 0;
for (id<MPExpressionElement> element in elements) {
length += element.length;
}
return length;
}
- (NSUInteger)indexOfElementAtLocation:(NSUInteger)location
{
NSUInteger index = 0;
[self calculateSplitOffsetForSplitLocation:location inElementAtIndex:&index];
return index;
}
- (void)validateElements:(NSArray *)elements
{
for (id element in elements) {
@@ -89,8 +108,9 @@
@implementation MPExpression {
NSUInteger _cachedLength;
NSRange editedRange;
NSRange replacementRange;
NSRange _editedRange;
BOOL _didSplitEndOnEditing;
NSUInteger _replacementLength;
}
@synthesize elements = _elements;
@@ -111,7 +131,6 @@
self = [super init];
if (self) {
_cachedLength = 0;
_elements = [[NSMutableArray alloc] initWithCapacity:elements.count];
_elements = [[NSMutableArray alloc] initWithArray:elements
copyItems:YES];
[self fixElements];
@@ -123,29 +142,29 @@
- (void)fixElements
{
for (NSUInteger index = 0; index < self.elements.count; index++) {
id<MPExpressionElement> next = index+1 < self.elements.count ? self.elements[index+1] :nil;
id<MPExpressionElement> next = index+1 < self.elements.count ? self.elements[index+1] : nil;
id<MPExpressionElement> current = self.elements[index];
if ([current isString]) {
if (current.length == 0) {
[self.elements removeObjectAtIndex:index];
if (index < replacementRange.location) {
replacementRange.location--;
} else if (index < NSMaxRange(replacementRange)-1) {
replacementRange.length--;
} else if (index == NSMaxRange(replacementRange)) {
editedRange.length++;
if (index >= _editedRange.location && index < NSMaxRange(_editedRange)) {
--_replacementLength;
}
--index;
} else if ([next isString]) {
NSString *new = [NSString stringWithFormat:@"%@%@", current, next];
[self.elements replaceObjectsInRange:NSMakeRange(index, 2)
withObjectsFromArray:@[new]];
if (index < replacementRange.location) {
replacementRange.location--;
} else if (index < NSMaxRange(replacementRange)-1) {
replacementRange.length--;
} else if (index == NSMaxRange(replacementRange)-1) {
editedRange.length++;
NSUInteger maxReplacementIndex = _editedRange.location + _replacementLength;
if (index == _editedRange.location - 1) {
--_editedRange.location;
++_editedRange.length;
} else if (index >= _editedRange.location && index < maxReplacementIndex - 1) {
--_replacementLength;
} else if (index == maxReplacementIndex - 1) {
if (!_didSplitEndOnEditing) {
++_editedRange.length;
}
}
--index;
}
@@ -159,9 +178,7 @@
- (NSUInteger)length
{
if (_cachedLength == 0) {
for (id<MPExpressionElement> element in self.elements) {
_cachedLength += element.length;
}
_cachedLength = [self lengthOfElements:self.elements];
}
return _cachedLength;
}
@@ -171,9 +188,20 @@
return self.elements.count;
}
- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)index
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx
{
return self.elements[index];
[self replaceSymbolsInRange:NSMakeRange(idx, 1)
withElements:@[obj]];
}
- (id)objectAtIndexedSubscript:(NSUInteger)idx
{
return [self elementAtIndex:idx];
}
- (id<MPExpressionElement>)elementAtIndex:(NSUInteger)anIndex
{
return self.elements[anIndex];
}
- (NSArray *)elementsInRange:(NSRange)range
@@ -181,12 +209,13 @@
return [self.elements subarrayWithRange:range];
}
#warning If multiple equal expressions exist errors may occur...
- (NSUInteger)indexOfElement:(id<MPExpressionElement>)element
{
return [self.elements indexOfObject:element];
}
- (void)replaceElementsInRange:(NSRange)range
- (void)replaceSymbolsInRange:(NSRange)range
withElements:(NSArray *)elements
{
if (NSMaxRange(range) > self.length) {
@@ -197,7 +226,7 @@
[self validateElements:elements];
// Locate the position, split the elements
NSUInteger startIndex;
NSUInteger startIndex; // startIndex is inclusive
BOOL didSplitStart = NO;
if ([self numberOfElements] == 0) {
startIndex = 0;
@@ -205,27 +234,30 @@
didSplitStart = [self splitElementsAtLocation:range.location
insertionIndex:&startIndex];
}
NSUInteger endIndex;
NSUInteger endIndex; // endIndex is exclusive
BOOL didSplitEnd = [self splitElementsAtLocation:NSMaxRange(range)
insertionIndex:&endIndex];
// Perform the replacement
NSArray *newElements = [[NSArray alloc] initWithArray:elements
NSMutableArray *newElements = [[NSMutableArray alloc] initWithArray:elements
copyItems:YES];
[self.elements replaceObjectsInRange:NSMakeRange(startIndex, endIndex-startIndex)
withObjectsFromArray:newElements];
_cachedLength = 0;
NSUInteger editingStart = startIndex - (didSplitStart?1:0);
NSUInteger editingLength = endIndex - startIndex + (didSplitStart?1:0) + (didSplitEnd?1:0);
editedRange = NSMakeRange(editingStart, editingLength);
replacementRange = NSMakeRange(startIndex, elements.count);
NSUInteger editLocation = startIndex - (didSplitStart ? 1 : 0);
NSUInteger editLength = range.length > 0 ? (endIndex - startIndex) : 0;
_editedRange = NSMakeRange(editLocation, editLength);
_didSplitEndOnEditing = didSplitEnd;
_replacementLength = elements.count + (didSplitStart ? 1 : 0) + (didSplitEnd ? 1 : 0);
[self fixElements];
MPRangePath *changePath = [[MPRangePath alloc] initWithRange:editedRange];
[self didChangeElementsInRangePath:changePath replacementLength:replacementRange.length];
[self didChangeElementsInRangePath:[[MPRangePath alloc] initWithRange:_editedRange]
replacementLength:_replacementLength];
}
- (double)doubleValue
{
#warning Unimplemented Method
@@ -266,6 +298,7 @@
- (NSString *)description
{
#warning Bad Implementation
NSMutableString *description = [[NSMutableString alloc] init];
NSUInteger index = 0;
for (id element in self.elements) {
@@ -347,6 +380,16 @@
return [targetExpression elementsInRange:rangePath.rangeAtLastIndex];
}
- (NSIndexPath *)indexPath
{
if (self.parent) {
NSUInteger selfIndex = [self.parent indexOfChild:self];
return [[self.parent indexPath] indexPathByAddingIndex:selfIndex];
} else {
return [[NSIndexPath alloc] init];
}
}
#pragma mark Working With Expressions
- (MPExpression *)subexpressionFromLocation:(NSUInteger)from
{
@@ -377,7 +420,7 @@
- (void)appendElements:(NSArray *)elements
{
[self replaceElementsInRange:NSMakeRange(self.length, 0) withElements:elements];
[self replaceSymbolsInRange:NSMakeRange(self.length, 0) withElements:elements];
}
- (void)insertElement:(id<MPExpressionElement>)anElement atLocation:(NSUInteger)index
@@ -387,12 +430,12 @@
- (void)insertElements:(NSArray *)elements atLocation:(NSUInteger)index
{
[self replaceElementsInRange:NSMakeRange(index, 0) withElements:elements];
[self replaceSymbolsInRange:NSMakeRange(index, 0) withElements:elements];
}
- (void)deleteElementsInRange:(NSRange)range
{
[self replaceElementsInRange:range withElements:@[]];
[self replaceSymbolsInRange:range withElements:@[]];
}
#pragma mark Evaluating Expressions

View File

@@ -8,7 +8,7 @@
@import Foundation;
@protocol MPExpressionElement <NSObject, NSCoding>
@protocol MPExpressionElement <NSObject, NSCopying, NSCoding>
- (BOOL)isString;
- (BOOL)isFunction;

View File

@@ -22,6 +22,7 @@
#pragma mark Working With the Expression Tree
@property (nonatomic, weak) MPExpression *parent; // Documentation: Do not set
- (NSIndexPath *)indexPath;
- (NSUInteger)numberOfChildren; // Override
- (MPExpression *)childAtIndex:(NSUInteger)index; // Override

View File

@@ -24,6 +24,12 @@
}
#pragma mark Working With the Expression Tree
- (NSIndexPath *)indexPath
{
NSUInteger selfIndex = [self.parent indexOfElement:self];
return [[self.parent indexPath] indexPathByAddingIndex:selfIndex];
}
- (NSUInteger)numberOfChildren
{
return 0;

View File

@@ -99,7 +99,7 @@
#pragma mark Evaluating Functions
- (double)doubleValue
{
#warning Implementation
#warning Unimplemented Method
return 0;
}

View File

@@ -218,15 +218,20 @@
XCTAssertEqual([testExpression numberOfElements], 3);
// 12678 [] 90
[testExpression deleteElementsInRange:NSMakeRange(0, 2)];
XCTAssertEqual([testExpression numberOfElements], 3);
XCTAssertEqualObjects([testExpression elementAtIndex:0], @"678");
[testExpression insertElement:[[MPFunction alloc] init]
atLocation:2];
XCTAssertEqual([testExpression numberOfElements], 5);
// 12 [] 678 [] 90
// 67 [] 8 [] 90
[testExpression replaceElementsInRange:NSMakeRange(2, 5)
[testExpression replaceSymbolsInRange:NSMakeRange(2, 5)
withElements:@[[[MPFunction alloc] init]]];
XCTAssertEqual([testExpression numberOfElements], 3);
// 12 [] 90
XCTAssertEqual([testExpression numberOfElements], 2);
XCTAssertEqualObjects([testExpression elementAtIndex:0], @"67");
// 67 []
}
- (void)testInvalidMutatingRange {