diff --git a/MathPad/MPExpression.h b/MathPad/MPExpression.h index f0433c4..8d4d821 100644 --- a/MathPad/MPExpression.h +++ b/MathPad/MPExpression.h @@ -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 + #pragma mark Creation Methods -- (instancetype)init; // Convenience -- (instancetype)initWithElement:(id)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)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)elementAtIndex:(NSUInteger)index; -- (NSArray *)elementsInRange:(NSRange)range; -- (NSUInteger)indexOfElement:(id)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)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)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 + + +/*! + @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)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)anElement atLocation:(NSUInteger)index; diff --git a/MathPad/MPExpression.m b/MathPad/MPExpression.m index 7476c66..d427803 100644 --- a/MathPad/MPExpression.m +++ b/MathPad/MPExpression.m @@ -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 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 next = index+1 < self.elements.count ? self.elements[index+1] :nil; + id next = index+1 < self.elements.count ? self.elements[index+1] : nil; id 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 element in self.elements) { - _cachedLength += element.length; - } + _cachedLength = [self lengthOfElements:self.elements]; } return _cachedLength; } @@ -171,9 +188,20 @@ return self.elements.count; } -- (id)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)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)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 - copyItems:YES]; + 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)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 diff --git a/MathPad/MPExpressionElement.h b/MathPad/MPExpressionElement.h index 3198f85..1aa7f57 100644 --- a/MathPad/MPExpressionElement.h +++ b/MathPad/MPExpressionElement.h @@ -8,7 +8,7 @@ @import Foundation; -@protocol MPExpressionElement +@protocol MPExpressionElement - (BOOL)isString; - (BOOL)isFunction; diff --git a/MathPad/MPFunction.h b/MathPad/MPFunction.h index 13a6215..4394886 100644 --- a/MathPad/MPFunction.h +++ b/MathPad/MPFunction.h @@ -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 diff --git a/MathPad/MPFunction.m b/MathPad/MPFunction.m index 1d99321..3e66ebe 100644 --- a/MathPad/MPFunction.m +++ b/MathPad/MPFunction.m @@ -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; diff --git a/MathPad/MPSumFunction.m b/MathPad/MPSumFunction.m index 2240e4c..4e9822f 100644 --- a/MathPad/MPSumFunction.m +++ b/MathPad/MPSumFunction.m @@ -99,7 +99,7 @@ #pragma mark Evaluating Functions - (double)doubleValue { -#warning Implementation +#warning Unimplemented Method return 0; } diff --git a/MathPadTests/MPExpressionTests.m b/MathPadTests/MPExpressionTests.m index 7902fb4..d5c8eb6 100644 --- a/MathPadTests/MPExpressionTests.m +++ b/MathPadTests/MPExpressionTests.m @@ -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 {