//
// MPExpression.m
// MathPad
//
// Created by Kim Wittenburg on 10.08.14.
// Copyright (c) 2014 Kim Wittenburg. All rights reserved.
//
#import "MPExpression.h"
#import "MPExpressionElement.h"
#import "MPFunction.h"
#import "MPRangePath.h"
#import "MPExpressionTokenizer.h"
#import "MPToken.h"
#import "MPExpressionParser.h"
#import "MPParsedExpression.h"
#import "NSIndexPath+MPAdditions.h"
NSString *const MPIllegalElementException = @"Illegal Element Exception";
NSString *const MPIllegalElementExceptionElementKey = @"MPIllegalElementExceptionElementKey";
@interface MPExpression () {
NSMutableArray * _elements;
}
/*!
@method tokens
@brief Private method. Returns an array containing all tokens from the
receiver.
@return All items in the MPTokenReferenceFrame.
*/
- (NSArray *)tokens;
/*!
@method validateElements:
@brief Private method. Checks whether all objects in the specified array
are valid expression elements.
@discussion If an object is not valid a
MPIllegalElementException is raised.
@param elements
The array of objects to be validated.
*/
- (void)validateElements:(NSArray *)elements;
/*!
@method fixElements
@brief Private method. Restores consistency in the receiver after a
change was made.
*/
- (void)fixElements;
/*!
@method _replaceSymbolsInRange:withElements:
@brief Private method. Replaces the symbols in the specified range with
the elements from the elements array.
@discussion This is the most primitive mutation method of the @c MPExpression
class.
@param range
The range of symbols to be replaced. The range is specified in
the symbol reference frame. If the range exceeds the receiver's
bounds a NSRangeException is raised.
@param elements
The elements that should replace the symbols in the specified
range.
*/
- (void)_replaceSymbolsInRange:(NSRange)range
withElements:(NSArray *)elements;
/*!
@method _splitElementsAtLocation:insertionIndex:
@brief Splits the receiver's elements at the specified
location.
@discussion The split location can be inside a string element. In that case
the string is replaced with two other smaller strings.
Splitting the elements destroys the receiver's integrity. The
receiver of this message must also receive a
-fixElements message soon afterwards.
@param location
The index where to split the elements. Specified in the symbol
reference frame.
@param insertionIndex
Splitting elements may shift the indexes of the following
elements. This parameter is set to the index (specified in the
element reference frame) that should be used to insert a new
element at the location where the elements were previously
splitted.
@return @c YES if a string element was split into two smaller strings,
NO if the split location corresponds to
a location between two elements.
*/
- (BOOL)_splitElementsAtLocation:(NSUInteger)location
insertionIndex:(NSUInteger *)insertionIndex;
@end
@implementation MPExpression {
NSArray *_tokenCache;
NSRange _editedRange;
BOOL _didSplitStartOnEditing;
BOOL _didSplitEndOnEditing;
NSUInteger _replacementLength;
}
#pragma mark Creation Methods
- (instancetype)init
{
return [self initWithElements:@[]];
}
- (instancetype)initWithElement:(id)element
{
return [self initWithElements:@[element]];
}
- (instancetype)initWithElements:(NSArray *)elements
{
self = [super init];
if (self) {
[self validateElements:elements];
_elements = [[NSMutableArray alloc] initWithArray:elements
copyItems:YES];
[self fixElements];
}
return self;
}
#pragma mark Private Methods
- (NSArray *)tokens
{
if (!_tokenCache) {
_tokenCache = [MPExpressionTokenizer tokenizeExpression:self];
}
return _tokenCache;
}
- (void)validateElements:(NSArray *)elements
{
for (id element in elements) {
if (![element conformsToProtocol:@protocol(MPExpressionElement)]) {
@throw [NSException exceptionWithName:MPIllegalElementException
reason:@"Elements must conform to the MPExpressionElement protocol."
userInfo:@{MPIllegalElementExceptionElementKey: element}];
}
}
}
- (void)fixElements
{
for (NSUInteger index = 0; index < _elements.count; index++) {
id next = index+1 < _elements.count ? _elements[index+1] : nil;
id current = _elements[index];
if ([current isString]) {
if (current.length == 0) {
[_elements removeObjectAtIndex:index];
if (index >= _editedRange.location && index < NSMaxRange(_editedRange)) {
--_replacementLength;
}
--index;
} else if ([next isString]) {
NSString *new = [NSString stringWithFormat:@"%@%@", current, next];
[_elements replaceObjectsInRange:NSMakeRange(index, 2)
withObjectsFromArray:@[new]];
NSUInteger maxReplacementIndex = _editedRange.location + _replacementLength;
if (index == _editedRange.location-1 && !_didSplitStartOnEditing) {
--_editedRange.location;
++_editedRange.length;
if (index == maxReplacementIndex-1 && !_didSplitEndOnEditing) {
++_editedRange.length;
++_replacementLength;
}
} else if (index >= _editedRange.location && index < maxReplacementIndex - 1) {
--_replacementLength;
} else if (index == maxReplacementIndex-1 && !_didSplitEndOnEditing) {
++_editedRange.length;
}
--index;
}
} else {
[(MPFunction *)current setParent:self];
}
}
}
#pragma mark Querying Expressions
- (MPExpression *)rootExpression
{
if (self.parent == nil) {
return self;
}
return [self.parent rootExpression];
}
- (NSIndexPath *)indexPath
{
if (self.parent) {
NSUInteger selfIndex = [self.parent indexOfChild:self];
return [[self.parent indexPath] indexPathByAddingIndex:selfIndex];
} else {
return [[NSIndexPath alloc] init];
}
}
- (NSUInteger)countItemsInReferenceFrame:(MPReferenceFrame)referenceFrame
{
switch (referenceFrame) {
case MPElementReferenceFrame:
return _elements.count;
case MPSymbolReferenceFrame:
{
NSUInteger count = 0;
for (id element in _elements) {
count += element.length;
}
return count;
}
case MPTokenReferenceFrame:
return self.tokens.count;
}
}
- (id)itemAtIndex:(NSUInteger)anIndex
referenceFrame:(MPReferenceFrame)referenceFrame
{
switch (referenceFrame) {
case MPElementReferenceFrame:
return _elements[anIndex];
case MPSymbolReferenceFrame:
{
NSUInteger offsetInElement;
NSUInteger elementIndex = [self convertIndex:anIndex
fromReferenceFrame:MPSymbolReferenceFrame
toReferenceFrame:MPElementReferenceFrame
offset:&offsetInElement];
id element = [self elementAtIndex:elementIndex];
if ([element isFunction]) {
return element;
} else {
return [((NSString *)element) substringWithRange:NSMakeRange(offsetInElement, 1)];
}
}
case MPTokenReferenceFrame:
return self.tokens[anIndex];
}
}
- (id)elementAtIndex:(NSUInteger)anIndex
referenceFrame:(MPReferenceFrame)referenceFrame
{
NSUInteger elementIndex = [self convertIndex:anIndex
fromReferenceFrame:referenceFrame
toReferenceFrame:MPElementReferenceFrame];
return _elements[elementIndex];
}
#warning If multiple equal expressions exist errors may occur...
- (NSUInteger)indexOfElement:(id)element
{
return [_elements indexOfObject:element];
}
- (NSArray *)itemsInRange:(NSRange)aRange
referenceFrame:(MPReferenceFrame)referenceFrame
{
MPExpression *subexpression = [self subexpressionWithRange:aRange
referenceFrame:referenceFrame];
return [subexpression allItemsInReferenceFrame:referenceFrame];
}
- (NSArray *)allItemsInReferenceFrame:(MPReferenceFrame)referenceFrame
{
switch (referenceFrame) {
case MPElementReferenceFrame:
return _elements;
case MPSymbolReferenceFrame:
{
NSMutableArray *symbols = [[NSMutableArray alloc] init];
for (id element in _elements) {
if ([element isString]) {
for (NSUInteger i = 0; i < [element length]; i++) {
NSString *ichar = [NSString stringWithFormat:@"%c", [((NSString *)element) characterAtIndex:i]];
[symbols addObject:ichar];
}
} else {
[symbols addObject:element];
}
}
}
case MPTokenReferenceFrame:
return self.tokens;
}
}
- (id)elementAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.length == 0) {
return self;
}
id element = _elements[[indexPath indexAtPosition:0]];
if (indexPath.length == 1) {
return element;
}
if ([element isFunction]) {
return [(MPFunction *)element elementAtIndexPath:[indexPath indexPathByRemovingFirstIndex]];
}
// TODO: Raise appropriate exeption.
return nil;
}
- (NSUInteger)convertIndex:(NSUInteger)anIndex
fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
toReferenceFrame:(MPReferenceFrame)toReferenceFrame
{
return [self convertIndex:anIndex
fromReferenceFrame:fromReferenceFrame
toReferenceFrame:toReferenceFrame
offset:NULL];
}
- (NSUInteger)convertIndex:(NSUInteger)anIndex
fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
toReferenceFrame:(MPReferenceFrame)toReferenceFrame
offset:(NSUInteger *)offset
{
if (fromReferenceFrame == toReferenceFrame || anIndex == 0) {
if (offset) {
*offset = 0;
}
return anIndex;
}
NSUInteger symbolIndex __block = 0;
switch (fromReferenceFrame) {
case MPElementReferenceFrame:
[_elements enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
symbolIndex += obj.length;
*stop = idx >= anIndex - 1;
}];
break;
case MPSymbolReferenceFrame:
symbolIndex = anIndex;
break;
case MPTokenReferenceFrame:
[self.tokens enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
symbolIndex += obj.range.length;
*stop = idx >= anIndex - 1;
}];
break;
}
switch (toReferenceFrame) {
case MPElementReferenceFrame:
{
NSUInteger totalLength = 0;
NSUInteger elementIndex = 0;
id element;
while (totalLength < symbolIndex) {
element = _elements[elementIndex++];
totalLength += element.length;
}
--elementIndex;
NSUInteger offsetInElement = element.length - totalLength + symbolIndex;
if (offsetInElement == element.length) {
offsetInElement = 0;
elementIndex++;
}
if (offset) {
*offset = offsetInElement;
}
return elementIndex;
}
case MPSymbolReferenceFrame:
if (offset) {
*offset = 0;
}
return symbolIndex;
case MPTokenReferenceFrame:
{
NSUInteger totalLength = 0;
NSUInteger tokenIndex = 0;
id token;
while (totalLength < symbolIndex) {
token = self.tokens[tokenIndex++];
totalLength += token.range.length;
}
--tokenIndex;
NSUInteger offsetInToken = token.range.length - totalLength + symbolIndex;
if (offsetInToken == token.range.length) {
offsetInToken = 0;
tokenIndex++;
}
if (offset) {
*offset = offsetInToken;
}
return tokenIndex;
}
}
}
- (NSRange)convertRange:(NSRange)aRange
fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
toReferenceFrame:(MPReferenceFrame)toReferenceFrame
{
return [self convertRange:aRange
fromReferenceFrame:fromReferenceFrame
toReferenceFrame:toReferenceFrame
leadingOffset:NULL
trailingOffset:NULL];
}
- (NSRange)convertRange:(NSRange)aRange
fromReferenceFrame:(MPReferenceFrame)fromReferenceFrame
toReferenceFrame:(MPReferenceFrame)toReferenceFrame
leadingOffset:(NSUInteger *)leadingOffset
trailingOffset:(NSUInteger *)trailingOffset
{
NSUInteger start = [self convertIndex:aRange.location
fromReferenceFrame:fromReferenceFrame
toReferenceFrame:toReferenceFrame
offset:leadingOffset];
NSUInteger end = [self convertIndex:NSMaxRange(aRange)
fromReferenceFrame:fromReferenceFrame
toReferenceFrame:toReferenceFrame
offset:trailingOffset];
return NSMakeRange(start, end - start);
}
#pragma mark Mutating Expressions
- (void)replaceItemsInRange:(NSRange)aRange
referenceFrame:(MPReferenceFrame)referenceFrame
withElements:(NSArray *)elements
{
NSUInteger start = [self convertIndex:aRange.location
fromReferenceFrame:referenceFrame
toReferenceFrame:MPSymbolReferenceFrame];
NSUInteger end = [self convertIndex:NSMaxRange(aRange)
fromReferenceFrame:referenceFrame
toReferenceFrame:MPSymbolReferenceFrame];
[self _replaceSymbolsInRange:NSMakeRange(start, end - start)
withElements:elements];
}
- (void)_replaceSymbolsInRange:(NSRange)range
withElements:(NSArray *)elements
{
if (NSMaxRange(range) > [self countItemsInReferenceFrame:MPSymbolReferenceFrame]) {
@throw [NSException exceptionWithName:NSRangeException
reason:@"Range out of bounds of expression"
userInfo:nil];
}
[self validateElements:elements];
// Locate the position, split the elements
NSUInteger startIndex; // startIndex is inclusive
BOOL didSplitStart = NO;
if (_elements.count == 0) {
startIndex = 0;
} else {
didSplitStart = [self _splitElementsAtLocation:range.location
insertionIndex:&startIndex];
}
NSUInteger endIndex; // endIndex is exclusive
BOOL didSplitEnd = [self _splitElementsAtLocation:NSMaxRange(range)
insertionIndex:&endIndex];
// Perform the replacement
NSMutableArray *newElements = [[NSMutableArray alloc] initWithArray:elements
copyItems:YES];
NSArray *removedElements = [_elements subarrayWithRange:NSMakeRange(startIndex, endIndex-startIndex)];
[_elements replaceObjectsInRange:NSMakeRange(startIndex, endIndex-startIndex)
withObjectsFromArray:newElements];
for (id element in removedElements) {
if ([element isFunction]) {
((MPFunction *)element).parent = nil;
}
}
_tokenCache = nil;
NSUInteger editLocation = startIndex - (didSplitStart ? 1 : 0);
NSUInteger editLength = endIndex - startIndex;
if (range.length == 0 && didSplitStart) {
editLength = 1;
}
_editedRange = NSMakeRange(editLocation, editLength);
_didSplitStartOnEditing = didSplitStart;
_didSplitEndOnEditing = didSplitEnd;
_replacementLength = elements.count;
if (didSplitStart) {
_replacementLength++;
if (range.length == 0) {
_replacementLength++;
}
}
if (didSplitEnd) {
_replacementLength++;
}
[self fixElements];
[self changedElementsInRangePath:[[MPRangePath alloc] initWithRange:_editedRange]
replacementLength:_replacementLength];
}
- (BOOL)_splitElementsAtLocation:(NSUInteger)location
insertionIndex:(NSUInteger *)insertionIndex
{
if (location == 0) {
*insertionIndex = 0;
return NO;
}
NSUInteger splitOffset;
NSUInteger splitElementIndex = [self convertIndex:location
fromReferenceFrame:MPSymbolReferenceFrame
toReferenceFrame:MPElementReferenceFrame
offset:&splitOffset];
if (splitOffset != 0) {
NSString *splitElement = (NSString *)_elements[splitElementIndex];
NSString *leftPart = [splitElement substringToIndex:splitOffset];
NSString *rightPart = [splitElement substringFromIndex:splitOffset];
[_elements replaceObjectsInRange:NSMakeRange(splitElementIndex, 1)
withObjectsFromArray:@[leftPart, rightPart]];
++splitElementIndex;
}
*insertionIndex = splitElementIndex;
return splitOffset != 0;
}
- (void)changedElementsInRangePath:(MPRangePath *)rangePath
replacementLength:(NSUInteger)replacementLength
{
NSUInteger selfIndex = [self.parent indexOfChild:self];
MPRangePath *newPath = MPMakeRangePath([rangePath.location indexPathByPreceedingIndex:selfIndex], rangePath.length);
[self.parent didChangeElementsInRangePath:newPath
replacementLength:replacementLength];
}
- (MPExpression *)subexpressionFromIndex:(NSUInteger)from
referenceFrame:(MPReferenceFrame)referenceFrame
{
return [self subexpressionWithRange:NSMakeRange(from, [self countItemsInReferenceFrame:referenceFrame] - from)
referenceFrame:referenceFrame];
}
- (MPExpression *)subexpressionToIndex:(NSUInteger)to
referenceFrame:(MPReferenceFrame)referenceFrame
{
return [self subexpressionWithRange:NSMakeRange(0, to)
referenceFrame:referenceFrame];
}
- (MPExpression *)subexpressionWithRange:(NSRange)aRange
referenceFrame:(MPReferenceFrame)referenceFrame
{
MPExpression *subexpression = [self copy];
NSRange preceedingRange = NSMakeRange(0, aRange.location);
NSUInteger firstOut = NSMaxRange(aRange);
NSRange exceedingRange = NSMakeRange(firstOut, [self countItemsInReferenceFrame:referenceFrame] - firstOut);
[subexpression deleteElementsInRange:exceedingRange
referenceFrame:referenceFrame];
[subexpression deleteElementsInRange:preceedingRange
referenceFrame:referenceFrame];
return subexpression;
}
#pragma mark Evaluating Expressions
- (MPParsedExpression *)parse:(NSArray *__autoreleasing *)errors
{
return [self parseExpectingVariable:NO
errors:errors];
}
- (MPParsedExpression *)parseExpectingVariable:(BOOL)flag
errors:(NSArray *__autoreleasing *)errors
{
return [[[MPExpressionParser alloc] initWithExpression:self] parseExpectingVariableDefinition:flag
errors:errors];
}
#pragma mark Basic NSObject Methods
- (NSString *)description
{
#warning Bad Implementation
NSMutableString *description = [[NSMutableString alloc] init];
NSUInteger index = 0;
for (id element in _elements) {
if ([element isString]) {
NSMutableString *correctedSymbol = [[element stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
// Prefix operator
if (element != _elements[0]) {
unichar prefix = [correctedSymbol characterAtIndex:0];
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:prefix]) {
[correctedSymbol insertString:@"*"
atIndex:0];
}
}
// Suffix operator
if (element != [_elements lastObject]) {
unichar suffix = [correctedSymbol characterAtIndex:correctedSymbol.length-1];
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:suffix]) {
[correctedSymbol appendString:@"*"];
}
}
[description appendString:correctedSymbol];
} else if (index > 0 && [_elements[index-1] isKindOfClass:[MPFunction class]]) {
[description appendFormat:@"*%@", [element description]];
} else {
[description appendString:[element description]];
}
index++;
}
return description;
}
- (NSUInteger)hash
{
return [_elements hash];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone
{
MPExpression *copy = [[MPExpression allocWithZone:zone] initWithElements:_elements];
return copy;
}
#pragma mark - NSCoding
- (id)initWithCoder:(NSCoder *)aDecoder
{
// TODO: Test Coding
return [self initWithElements:[aDecoder decodeObject]];
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_elements];
}
@end
@implementation MPExpression (MPExpressionConvenience)
#pragma mark Querying Expressions
- (NSUInteger)countElements
{
return [self countItemsInReferenceFrame:MPElementReferenceFrame];
}
- (NSUInteger)countSymbols
{
return [self countItemsInReferenceFrame:MPSymbolReferenceFrame];
}
- (NSUInteger)countTokens
{
return [self countItemsInReferenceFrame:MPTokenReferenceFrame];
}
- (id)elementAtIndex:(NSUInteger)anIndex
{
return [self itemAtIndex:anIndex
referenceFrame:MPElementReferenceFrame];
}
- (id)symbolAtIndex:(NSUInteger)anIndex
{
return [self itemAtIndex:anIndex
referenceFrame:MPSymbolReferenceFrame];
}
- (id)tokenAtIndex:(NSUInteger)anIndex
{
return [self itemAtIndex:anIndex
referenceFrame:MPTokenReferenceFrame];
}
#pragma mark Mutating Expressions
- (void)appendElement:(id)anElement
{
[self appendElements:@[anElement]];
}
- (void)appendElements:(NSArray *)elements
{
[self replaceItemsInRange:NSMakeRange([self countItemsInReferenceFrame:MPSymbolReferenceFrame], 0)
referenceFrame:MPSymbolReferenceFrame
withElements:elements];
}
- (void)insertElement:(id)anElement
atIndex:(NSUInteger)index
referenceFrame:(MPReferenceFrame)referenceFrame
{
[self insertElements:@[anElement]
atIndex:index
referenceFrame:referenceFrame];
}
- (void)insertElements:(NSArray *)elements
atIndex:(NSUInteger)index
referenceFrame:(MPReferenceFrame)referenceFrame
{
[self replaceItemsInRange:NSMakeRange(index, 0)
referenceFrame:referenceFrame
withElements:elements];
}
- (void)deleteElementsInRange:(NSRange)range
referenceFrame:(MPReferenceFrame)referenceFrame
{
[self replaceItemsInRange:range
referenceFrame:referenceFrame
withElements:@[]];
}
@end