// // MPExpression.m // MathPad // // Created by Kim Wittenburg on 10.08.14. // Copyright (c) 2014 Kim Wittenburg. All rights reserved. // #import "MPExpression.h" #import "MPFunction.h" #import "MPRangePath.h" #import "NSIndexPath+MPAdditions.h" #import "MPException.h" @interface MPExpression () @property (readonly, nonatomic, strong) NSMutableArray *elements; @end @interface MPExpression (MPExpressionPrivate) - (NSUInteger)lengthOfElements:(NSArray *)elements; - (NSUInteger)indexOfElementAtLocation:(NSUInteger)location; - (void)validateElements:(NSArray *)elements; - (BOOL)splitElementsAtLocation:(NSUInteger)location insertionIndex:(out NSUInteger *)insertionIndex; - (NSUInteger)calculateSplitOffsetForSplitLocation:(NSUInteger)location inElementAtIndex:(out NSUInteger *)elementIndex; @end @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) { if (![element conformsToProtocol:@protocol(MPExpressionElement)]) { @throw [NSException exceptionWithName:MPIllegalElementException reason:@"Elements must conform to the MPExpressionElement protocol." userInfo:@{MPIllegalElementExceptionElementKey: element}]; } } } - (BOOL)splitElementsAtLocation:(NSUInteger)location insertionIndex:(out NSUInteger *)insertionIndex { if (location == 0) { *insertionIndex = 0; return NO; } NSUInteger splitElementIndex; NSUInteger splitOffset = [self calculateSplitOffsetForSplitLocation:location inElementAtIndex:&splitElementIndex]; id splitElement = self.elements[splitElementIndex]; if (splitOffset == splitElement.length) { splitOffset = 0; splitElementIndex++; } if (splitOffset != 0) { NSString *stringElement = (NSString *)splitElement; NSString *leftPart = [stringElement substringToIndex:splitOffset]; NSString *rightPart = [stringElement substringFromIndex:splitOffset]; [self.elements replaceObjectsInRange:NSMakeRange(splitElementIndex, 1) withObjectsFromArray:@[leftPart, rightPart]]; ++splitElementIndex; } *insertionIndex = splitElementIndex; return splitOffset != 0; } - (NSUInteger)calculateSplitOffsetForSplitLocation:(NSUInteger)location inElementAtIndex:(out NSUInteger *)elementIndex { NSUInteger length = 0; NSUInteger index = 0; NSUInteger elementLength = 0; for (id element in self.elements) { elementLength = element.length; length += elementLength; if (length >= location) { break; } ++index; } *elementIndex = index; return elementLength - (length - location); } @end @implementation MPExpression { NSUInteger _cachedLength; NSRange _editedRange; BOOL _didSplitEndOnEditing; NSUInteger _replacementLength; } @synthesize elements = _elements; #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) { _cachedLength = 0; _elements = [[NSMutableArray alloc] initWithArray:elements copyItems:YES]; [self fixElements]; } return self; } #pragma mark Working With the Expression Tree - (void)fixElements { for (NSUInteger index = 0; index < self.elements.count; index++) { 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 >= _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]]; 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; } } else { [(MPFunction *)current setParent:self]; } } } #pragma mark Primitive Methods - (NSUInteger)length { if (_cachedLength == 0) { _cachedLength = [self lengthOfElements:self.elements]; } return _cachedLength; } - (NSUInteger)numberOfElements { return self.elements.count; } - (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx { [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 { return [self.elements subarrayWithRange:range]; } #warning If multiple equal expressions exist errors may occur... - (NSUInteger)indexOfElement:(id)element { return [self.elements indexOfObject:element]; } - (void)replaceSymbolsInRange:(NSRange)range withElements:(NSArray *)elements { if (NSMaxRange(range) > self.length) { @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 ([self numberOfElements] == 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]; [self.elements replaceObjectsInRange:NSMakeRange(startIndex, endIndex-startIndex) withObjectsFromArray:newElements]; _cachedLength = 0; 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]; [self didChangeElementsInRangePath:[[MPRangePath alloc] initWithRange:_editedRange] replacementLength:_replacementLength]; } - (double)doubleValue { #warning Unimplemented Method return 0; } #pragma mark Notifications - (void)didChangeElementsInRangePath:(MPRangePath *)rangePath replacementLength:(NSUInteger)replacementLength { NSUInteger selfIndex = [self.parent indexOfChild:self]; MPRangePath *newPath = rangePath.copy; newPath.location = [newPath.location indexPathByPreceedingIndex:selfIndex]; [self.parent didChangeElementsInRangePath:newPath replacementLength:replacementLength]; } #pragma mark Basic NSObject Methods - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (object == nil) { return NO; } if (![object isKindOfClass:[MPExpression class]]) { return NO; } return [self isEqualToExpression:(MPExpression *)object]; } - (BOOL)isEqualToExpression:(MPExpression *)anExpression { return [self.elements isEqualToArray:anExpression.elements]; } - (NSString *)description { #warning Bad Implementation NSMutableString *description = [[NSMutableString alloc] init]; NSUInteger index = 0; for (id element in self.elements) { if ([element isString]) { NSMutableString *correctedSymbol = [[element stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy]; // Prefix operator if (element != self.elements[0]) { unichar prefix = [correctedSymbol characterAtIndex:0]; if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:prefix]) { [correctedSymbol insertString:@"*" atIndex:0]; } } // Suffix operator if (element != [self.elements lastObject]) { unichar suffix = [correctedSymbol characterAtIndex:correctedSymbol.length-1]; if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:suffix]) { [correctedSymbol appendString:@"*"]; } } [description appendString:correctedSymbol]; } else if (index > 0 && [self.elements[index-1] isKindOfClass:[MPFunction class]]) { [description appendFormat:@"*%@", [element description]]; } else { [description appendString:[element description]]; } index++; } return description; } - (NSUInteger)hash { return [self.elements hash]; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { MPExpression *copy = [[MPExpression allocWithZone:zone] initWithElements:self.elements]; return copy; } #pragma mark - NSCoding - (id)initWithCoder:(NSCoder *)aDecoder { // TODO: Test Coding return [self initWithElements:[aDecoder decodeObject]]; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.elements]; } @end @implementation MPExpression (MPExpressionExtension) #pragma mark Working With the Expression Tree - (id)elementAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.length == 0) { return self; } id element = [self elementAtIndex:[indexPath indexAtPosition:0]]; if (indexPath.length == 1) { return element; } if ([element isFunction]) { return [(MPFunction *)element elementAtIndexPath:[indexPath indexPathByRemovingLastIndex]]; } return nil; } - (NSArray *)elementsInRangePath:(MPRangePath *)rangePath { MPExpression *targetExpression = [self elementAtIndexPath:[rangePath.location indexPathByRemovingLastIndex]]; 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 { return [self subexpressionWithRange:NSMakeRange(from, self.length - from)]; } - (MPExpression *)subexpressionToLocation:(NSUInteger)to { return [self subexpressionWithRange:NSMakeRange(0, to)]; } - (MPExpression *)subexpressionWithRange:(NSRange)range { MPExpression *subexpression = [self copy]; NSRange preceedingRange = NSMakeRange(0, range.location); NSUInteger firstOut = NSMaxRange(range); NSRange exceedingRange = NSMakeRange(firstOut, self.length-firstOut); [subexpression deleteElementsInRange:exceedingRange]; [subexpression deleteElementsInRange:preceedingRange]; return subexpression; } #pragma mark Mutating Expressions - (void)appendElement:(id)anElement { [self appendElements:@[anElement]]; } - (void)appendElements:(NSArray *)elements { [self replaceSymbolsInRange:NSMakeRange(self.length, 0) withElements:elements]; } - (void)insertElement:(id)anElement atLocation:(NSUInteger)location { [self insertElements:@[anElement] atLocation:location]; } - (void)insertElements:(NSArray *)elements atLocation:(NSUInteger)location { [self replaceSymbolsInRange:NSMakeRange(location, 0) withElements:elements]; } - (void)deleteElementsInRange:(NSRange)range { [self replaceSymbolsInRange:range withElements:@[]]; } @end