Forráskód Böngészése

mostly working new validation system

Adam Shaw 7 éve
szülő
commit
ac701a32b8

+ 6 - 10
src/interactions-external/ExternalElementDragging.ts

@@ -12,7 +12,7 @@ import { DragMetaInput, DragMeta, parseDragMeta } from '../structs/drag-meta'
 import EventApi from '../api/EventApi'
 import { elementMatches } from '../util/dom-manip'
 import { enableCursor, disableCursor } from '../util/misc'
-import { isEventsValid, isSelectionValid, eventToDateSpan } from '../validation'
+import { isEventsValid } from '../validation'
 
 export type DragMetaGenerator = DragMetaInput | ((el: HTMLElement) => DragMetaInput)
 
@@ -70,15 +70,11 @@ export default class ExternalElementDragging {
           receivingCalendar
         )
 
-        // TODO: fix inefficiency of calling eventTupleToStore again, and eventToDateSpan
-        if (this.dragMeta.create) {
-          let droppableEventStore = eventTupleToStore(droppableEvent)
-
-          isInvalid = !isEventsValid(droppableEventStore, receivingCalendar)
-
-        } else { // treat non-event-creating drags as selection validation
-          isInvalid = !isSelectionValid(eventToDateSpan(droppableEvent.def, droppableEvent.instance), receivingCalendar)
-        }
+        isInvalid = !isEventsValid(
+          eventTupleToStore(droppableEvent), // TODO: fix inefficiency of calling eventTupleToStore again
+          receivingCalendar,
+          !this.dragMeta.create
+        )
 
         if (isInvalid) {
           droppableEvent = null

+ 20 - 5
src/plugin-system.ts

@@ -1,14 +1,16 @@
 import { reducerFunc } from './reducers/types'
-import { eventDefParserFunc } from './structs/event'
+import { eventDefParserFunc, EventDef } from './structs/event'
 import { eventDragMutationMassager } from './interactions/EventDragging'
 import { eventDefMutationApplier } from './structs/event-mutation'
-import { dateClickApiTransformer, dateSelectionApiTransformer } from './Calendar'
+import Calendar, { dateClickApiTransformer, dateSelectionApiTransformer } from './Calendar'
 import { dateSelectionJoinTransformer } from './interactions/DateSelecting'
 import { ViewConfigInputHash } from './structs/view-config'
 import { assignTo } from './util/object'
 import { ViewSpecTransformer, ViewSpec } from './structs/view-spec'
 import { ViewProps } from './View'
 import { CalendarComponentProps } from './CalendarComponent'
+import Splitter from './component/event-splitting'
+import { Constraint } from './validation'
 
 // TODO: easier way to add new hooks? need to update a million things
 
@@ -24,6 +26,7 @@ export interface PluginDefInput {
   viewConfigs?: ViewConfigInputHash
   viewSpecTransformers?: ViewSpecTransformer[]
   viewPropsTransformers?: ViewPropsTransformerClass[]
+  validationSplitter?: ValidationSplitterMeta
 }
 
 export interface PluginHooks {
@@ -37,6 +40,7 @@ export interface PluginHooks {
   viewConfigs: ViewConfigInputHash // TODO: parse before gets to this step?
   viewSpecTransformers: ViewSpecTransformer[]
   viewPropsTransformers: ViewPropsTransformerClass[]
+  validationSplitter: ValidationSplitterMeta | null
 }
 
 export interface PluginDef extends PluginHooks {
@@ -44,6 +48,14 @@ export interface PluginDef extends PluginHooks {
   deps: PluginDef[]
 }
 
+
+export interface ValidationSplitterMeta {
+  splitterClass: new() => Splitter
+  getDateSpanPropsForKey: (key: string) => any
+  constraintAllowsKey: (constraint: Constraint, key: string) => boolean
+  eventAllowsKey: (subjectDef: EventDef, calendar: Calendar, currentSegmentKey: string) => boolean
+}
+
 export type ViewPropsTransformerClass = new() => ViewPropsTransformer
 
 export interface ViewPropsTransformer {
@@ -66,7 +78,8 @@ export function createPlugin(input: PluginDefInput): PluginDef {
     dateSelectionApiTransformers: input.dateSelectionApiTransformers || [],
     viewConfigs: input.viewConfigs || {},
     viewSpecTransformers: input.viewSpecTransformers || [],
-    viewPropsTransformers: input.viewPropsTransformers || []
+    viewPropsTransformers: input.viewPropsTransformers || [],
+    validationSplitter: input.validationSplitter || null
   }
 }
 
@@ -86,7 +99,8 @@ export class PluginSystem {
       dateSelectionApiTransformers: [],
       viewConfigs: {},
       viewSpecTransformers: [],
-      viewPropsTransformers: []
+      viewPropsTransformers: [],
+      validationSplitter: null
     }
     this.addedHash = {}
   }
@@ -116,6 +130,7 @@ function combineHooks(hooks0: PluginHooks, hooks1: PluginHooks): PluginHooks {
     dateSelectionApiTransformers: hooks0.dateSelectionApiTransformers.concat(hooks1.dateSelectionApiTransformers),
     viewConfigs: assignTo({}, hooks0.viewConfigs, hooks1.viewConfigs),
     viewSpecTransformers: hooks0.viewSpecTransformers.concat(hooks1.viewSpecTransformers),
-    viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers)
+    viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
+    validationSplitter: hooks1.validationSplitter || hooks0.validationSplitter
   }
 }

+ 2 - 1
src/reducers/eventStore.ts

@@ -169,7 +169,8 @@ function excludeEventsBySourceId(eventStore, sourceId) {
 }
 
 
-function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
+// QUESTION: why not just return instances? do a general object-property-exclusion util
+export function excludeInstances(eventStore: EventStore, removals: EventInstanceHash): EventStore {
   return {
     defs: eventStore.defs,
     instances: filterHash(eventStore.instances, function(instance: EventInstance) {

+ 7 - 18
src/structs/date-span.ts

@@ -108,10 +108,14 @@ export function isDateSpansEqual(span0: DateSpan, span1: DateSpan): boolean {
 }
 
 // the NON-DATE-RELATED props
-export function isSpanPropsEqual(span0: DateSpan, span1: DateSpan): boolean {
+function isSpanPropsEqual(span0: DateSpan, span1: DateSpan): boolean {
 
-  if (!isSpanPropsMatching(span0, span1)) {
-    return false
+  for (let propName in span1) {
+    if (propName !== 'range' && propName !== 'allDay') {
+      if (span0[propName] !== span1[propName]) {
+        return false
+      }
+    }
   }
 
   // are there any props that span0 has that span1 DOESN'T have?
@@ -125,21 +129,6 @@ export function isSpanPropsEqual(span0: DateSpan, span1: DateSpan): boolean {
   return true
 }
 
-// does subjectSpan have all the props/values that matchSpan does?
-// subjectSpan is allowed to have more
-export function isSpanPropsMatching(subjectSpan: DateSpan, matchSpan: DateSpan): boolean {
-
-  for (let propName in matchSpan) {
-    if (propName !== 'range' && propName !== 'allDay') {
-      if (subjectSpan[propName] !== matchSpan[propName]) {
-        return false
-      }
-    }
-  }
-
-  return true
-}
-
 export function buildDateSpanApi(span: DateSpan, dateEnv: DateEnv): DateSpanApi {
   return {
     start: dateEnv.toDate(span.range.start),

+ 1 - 17
src/structs/event-store.ts

@@ -113,7 +113,7 @@ export function getRelevantEvents(eventStore: EventStore, instanceId: string): E
   return createEmptyEventStore()
 }
 
-export function isEventDefsGrouped(def0: EventDef, def1: EventDef): boolean {
+function isEventDefsGrouped(def0: EventDef, def1: EventDef): boolean {
   return Boolean(def0.groupId && def0.groupId === def1.groupId)
 }
 
@@ -172,19 +172,3 @@ export function filterEventStoreDefs(eventStore: EventStore, filterFunc: (eventD
   })
   return { defs, instances }
 }
-
-// bad name. called "map" and returns an array
-export function mapEventInstances(
-  eventStore: EventStore,
-  callback: (instance: EventInstance, def: EventDef) => any
-): any[] {
-  let { defs, instances } = eventStore
-  let res = []
-
-  for (let instanceId in instances) {
-    let instance = instances[instanceId]
-    res.push(callback(instance, defs[instance.defId]))
-  }
-
-  return res
-}

+ 306 - 144
src/validation.ts

@@ -1,11 +1,13 @@
-import { EventStore, expandRecurring, eventTupleToStore, mapEventInstances, filterEventStoreDefs, isEventDefsGrouped } from './structs/event-store'
+import { EventStore, expandRecurring, eventTupleToStore, filterEventStoreDefs, createEmptyEventStore } from './structs/event-store'
 import Calendar from './Calendar'
-import { DateSpan, parseOpenDateSpan, OpenDateSpanInput, OpenDateSpan, isSpanPropsEqual, isSpanPropsMatching, buildDateSpanApi, DateSpanApi } from './structs/date-span'
+import { DateSpan, parseOpenDateSpan, OpenDateSpanInput, OpenDateSpan, buildDateSpanApi, DateSpanApi } from './structs/date-span'
 import { EventInstance, EventDef, EventTuple, parseEvent } from './structs/event'
-import { rangeContainsRange, rangesIntersect } from './datelib/date-range'
+import { rangeContainsRange, rangesIntersect, DateRange, OpenDateRange } from './datelib/date-range'
 import EventApi from './api/EventApi'
 import { EventUiHash } from './component/event-ui'
-import { compileEventUis } from './component/event-rendering';
+import { compileEventUis } from './component/event-rendering'
+import { ValidationSplitterMeta } from './plugin-system'
+import { excludeInstances } from './reducers/eventStore'
 
 // TODO: rename to "criteria" ?
 export type ConstraintInput = 'businessHours' | string | OpenDateSpanInput | { [timeOrRecurringProp: string]: any }
@@ -13,219 +15,379 @@ export type Constraint = 'businessHours' | string | OpenDateSpan | EventTuple
 export type Overlap = boolean | ((stillEvent: EventApi, movingEvent: EventApi | null) => boolean)
 export type Allow = (span: DateSpanApi, movingEvent: EventApi | null) => boolean
 
-interface ValidationEntity {
-  dateSpan: DateSpan
-  event: EventTuple | null
-  constraints: Constraint[]
-  overlaps: Overlap[]
-  allows: Allow[]
-}
 
-export function isEventsValid(eventStore: EventStore, calendar: Calendar): boolean {
-  let eventUis = compileEventUis(eventStore.defs, calendar.eventUiBases)
+// high-level segmenting-aware tester functions
+// ------------------------------------------------------------------------------------------------------------------------
+
+export function isEventsValid(subjectEventStore: EventStore, calendar: Calendar, isntEvent?: boolean): boolean {
+  let splitterMeta = calendar.pluginSystem.hooks.validationSplitter
+  let relevantSegmentedProps = getRelevantSegmentedProps(calendar, splitterMeta)
+  let subjectSegmentedProps = splitMinimalProps({
+    eventStore: subjectEventStore,
+    eventUiBases: isntEvent ? { '': calendar.selectionConfig } : calendar.eventUiBases,
+  }, splitterMeta)
+
+  for (let key in subjectSegmentedProps) {
+    let subjectProps = subjectSegmentedProps[key]
+    let relevantProps = relevantSegmentedProps[key]
+
+    if (!isSegmentedEventsValid(
+      subjectProps.eventStore,
+      subjectProps.eventUiBases,
+      relevantProps.eventStore,
+      relevantProps.eventUiBases,
+      relevantProps.businessHours,
+      calendar,
+      splitterMeta,
+      key,
+      isntEvent
+    )) {
+      return false
+    }
+  }
 
-  return isEntitiesValid(
-    eventStoreToEntities(eventStore, eventUis),
-    calendar
-  )
+  return true
 }
 
 export function isSelectionValid(selection: DateSpan, calendar: Calendar): boolean {
-  let { selectionConfig } = calendar
-
-  return isEntitiesValid(
-    [ {
-      dateSpan: selection,
-      event: null,
-      constraints: selectionConfig.constraints,
-      overlaps: selectionConfig.overlaps,
-      allows: selectionConfig.allows
-    } ],
-    calendar
-  )
+  let splitterMeta = calendar.pluginSystem.hooks.validationSplitter
+  let relevantSegmentedProps = getRelevantSegmentedProps(calendar, splitterMeta)
+  let subjectSegmentedProps = splitMinimalProps({
+    dateSelection: selection
+  }, splitterMeta)
+
+  for (let key in subjectSegmentedProps) {
+    let subjectProps = subjectSegmentedProps[key]
+    let relevantProps = relevantSegmentedProps[key]
+
+    if (!isSegmentedSelectionValid(
+      subjectProps.dateSelection,
+      relevantProps.eventStore,
+      relevantProps.businessHours,
+      calendar,
+      splitterMeta,
+      key,
+    )) {
+      return false
+    }
+  }
+
+  return true
 }
 
-function isEntitiesValid(entities: ValidationEntity[], calendar: Calendar): boolean {
+// we have a different meaning of "relevant" here than other places of codebase
+function getRelevantSegmentedProps(calendar: Calendar, splitterMeta: ValidationSplitterMeta) {
+  let view = calendar.view // yuck
 
-  for (let entity of entities) {
-    for (let constraint of entity.constraints) {
-      if (!isDateSpanWithinConstraint(entity.dateSpan, constraint, calendar)) {
+  return splitMinimalProps({
+    eventStore: calendar.state.eventStore,
+    eventUiBases: calendar.eventUiBases,
+    businessHours: view ? view.props.businessHours : createEmptyEventStore() // yuck
+  }, splitterMeta)
+}
+
+
+// insular tester functions
+// ------------------------------------------------------------------------------------------------------------------------
+
+function isSegmentedEventsValid(
+  subjectEventStore: EventStore,
+  subjectConfigBase: EventUiHash,
+  relevantEventStore: EventStore, // include the original subject events
+  relevantEventConfigBase: EventUiHash,
+  businessHoursUnexpanded: EventStore,
+  calendar: Calendar,
+  splitterMeta: ValidationSplitterMeta | null,
+  currentSegmentKey: string,
+  isntEvent: boolean
+): boolean {
+  let subjectDefs = subjectEventStore.defs
+  let subjectInstances = subjectEventStore.instances
+  let subjectConfigs = compileEventUis(subjectDefs, subjectConfigBase)
+  let otherEventStore = excludeInstances(relevantEventStore, subjectInstances) // exclude the subject events. TODO: exclude defs too?
+  let otherDefs = otherEventStore.defs
+  let otherInstances = otherEventStore.instances
+  let otherConfigs = compileEventUis(otherDefs, relevantEventConfigBase)
+
+  for (let subjectInstanceId in subjectInstances) {
+    let subjectInstance = subjectInstances[subjectInstanceId]
+    let subjectRange = subjectInstance.range
+    let subjectConfig = subjectConfigs[subjectInstance.defId]
+    let subjectDef = subjectDefs[subjectInstance.defId]
+    let { constraints, overlaps, allows } = subjectConfig
+
+    if (splitterMeta && !splitterMeta.eventAllowsKey(subjectDef, calendar, currentSegmentKey)) { // TODO: pass in EventUi
+      return false
+    }
+
+    // constraint
+    for (let constraint of constraints) {
+
+      if (!constraintPasses(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar)) {
+        return false
+      }
+
+      if (splitterMeta && !splitterMeta.constraintAllowsKey(constraint, currentSegmentKey)) {
         return false
       }
     }
-  }
 
-  // is this efficient?
-  let eventUis = compileEventUis(calendar.renderableEventStore.defs, calendar.eventUiBases)
-  let eventEntities = eventStoreToEntities(calendar.state.eventStore, eventUis)
-
-  for (let subjectEntity of entities) {
-    for (let eventEntity of eventEntities) {
-      if (considerEntitiesForOverlap(eventEntity, subjectEntity)) {
-
-        // the "subject" (the thing being dragged) must be an event if we are comparing it to other events for overlap
-        if (subjectEntity.event) {
-          for (let overlap of eventEntity.overlaps) {
-            if (!isOverlapValid(eventEntity.event, subjectEntity.event, overlap, calendar)) {
-              return false
-            }
-          }
+    // overlap
+    for (let otherInstanceId in otherInstances) {
+      let otherInstance = otherInstances[otherInstanceId]
+      let otherDef = otherDefs[otherInstance.defId]
+
+      // intersect! evaluate
+      if (rangesIntersect(subjectRange, otherInstance.range)) {
+        let otherOverlaps = otherConfigs[otherInstance.defId].overlaps
+
+        // consider the other event's overlap. only do this if the subject event is a "real" event
+        if (!isntEvent && !allOverlapsPass(otherOverlaps, otherDef, otherInstance, subjectDef, subjectInstance, calendar)) {
+          return false
         }
 
-        for (let overlap of subjectEntity.overlaps) {
-          if (!isOverlapValid(eventEntity.event, subjectEntity.event, overlap, calendar)) {
-            return false
-          }
+        if (!allOverlapsPass(overlaps, subjectDef, subjectInstance, otherDef, otherInstance, calendar)) {
+          return false
         }
       }
     }
-  }
 
-  for (let entity of entities) {
-    for (let allow of entity.allows) {
-      if (!isDateSpanAllowed(entity.dateSpan, entity.event, allow, calendar)) {
+    // allow (a function)
+    for (let allow of allows) {
+      let origDef = relevantEventStore.defs[subjectDef.defId]
+      let origInstance = relevantEventStore.instances[subjectInstanceId]
+
+      let subjectDateSpan: DateSpan = Object.assign(
+        {},
+        splitterMeta ? splitterMeta.getDateSpanPropsForKey(currentSegmentKey) : {},
+        { range: subjectInstance.range, allDay: subjectDef.allDay }
+      )
+
+      if (!allow(
+        buildDateSpanApi(subjectDateSpan, calendar.dateEnv),
+        new EventApi(calendar, origDef, origInstance)
+      )) {
         return false
       }
     }
+
   }
 
   return true
 }
 
-function considerEntitiesForOverlap(entity0: ValidationEntity, entity1: ValidationEntity) {
-  return ( // not comparing the same/related event
-    !entity0.event ||
-    !entity1.event ||
-    isEventsCollidable(entity0.event, entity1.event)
-  ) &&
-  dateSpansCollide(entity0.dateSpan, entity1.dateSpan) // a collision!
-}
+function isSegmentedSelectionValid(
+  selection: DateSpan,
+  relevantEventStore: EventStore,
+  businessHoursUnexpanded: EventStore,
+  calendar: Calendar,
+  splitterMeta: ValidationSplitterMeta | null,
+  currentSegmentKey: string
+): boolean {
+  let relevantInstances = relevantEventStore.instances
+  let relevantDefs = relevantEventStore.defs
+  let selectionRange = selection.range
+  let { constraints, overlaps, allows } = calendar.selectionConfig
+
+  // constraint
+  for (let constraint of constraints) {
+
+    if (!constraintPasses(constraint, selectionRange, relevantEventStore, businessHoursUnexpanded, calendar)) {
+      return false
+    }
 
-// do we want to compare these events for collision?
-// say no if events are the same, or if they share a groupId
-function isEventsCollidable(event0: EventTuple, event1: EventTuple): boolean {
-  if (event0.instance.instanceId === event1.instance.instanceId) {
-    return false
+    if (splitterMeta && !splitterMeta.constraintAllowsKey(constraint, currentSegmentKey)) {
+      return false
+    }
   }
 
-  return !isEventDefsGrouped(event0.def, event1.def)
-}
+  // overlap
+  for (let relevantInstanceId in relevantInstances) {
+    let relevantInstance = relevantInstances[relevantInstanceId]
+    let relevantDef = relevantDefs[relevantInstance.defId]
 
-function eventStoreToEntities(eventStore: EventStore, eventUis: EventUiHash): ValidationEntity[] {
-  return mapEventInstances(eventStore, function(eventInstance: EventInstance, eventDef: EventDef): ValidationEntity {
-    let eventUi = eventUis[eventDef.defId]
+    // intersect! evaluate
+    if (rangesIntersect(selectionRange, relevantInstance.range)) {
 
-    return {
-      dateSpan: eventToDateSpan(eventDef, eventInstance),
-      event: { def: eventDef, instance: eventInstance },
-      constraints: eventUi.constraints,
-      overlaps: eventUi.overlaps,
-      allows: eventUi.allows
+      if (!allOverlapsPass(overlaps, null, null, relevantDef, relevantInstance, calendar)) {
+        return false
+      }
     }
-  })
-}
-
-function isDateSpanWithinConstraint(subjectSpan: DateSpan, constraint: Constraint | null, calendar: Calendar): boolean {
-
-  if (constraint === null) {
-    return true // doesn't care
   }
 
-  let constrainingSpans: DateSpan[] = constraintToSpans(constraint, subjectSpan, calendar)
+  // allow (a function)
+  for (let allow of allows) {
 
-  for (let constrainingSpan of constrainingSpans) {
-    if (dateSpanContainsOther(constrainingSpan, subjectSpan)) {
-      return true
+    let fullDateSpan = Object.assign(
+      {},
+      splitterMeta ? splitterMeta.getDateSpanPropsForKey(currentSegmentKey) : {},
+      selection,
+    )
+
+    if (!allow(
+      buildDateSpanApi(fullDateSpan, calendar.dateEnv),
+      null
+    )) {
+      return false
     }
   }
 
-  return false // not contained by any one of the constrainingSpans
+  return true
+}
+
+
+// Constraint Utils
+// ------------------------------------------------------------------------------------------------------------------------
+
+function constraintPasses(
+  constraint: Constraint,
+  subjectRange: DateRange,
+  otherEventStore: EventStore,
+  businessHoursUnexpanded: EventStore,
+  calendar: Calendar
+) {
+  return anyRangesContainRange(
+    constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar),
+    subjectRange
+  )
 }
 
-function constraintToSpans(constraint: Constraint, subjectSpan: DateSpan, calendar: Calendar): DateSpan[] {
+function constraintToRanges(
+  constraint: Constraint,
+  subjectRange: DateRange, // for expanding a recurring constraint, or expanding business hours
+  otherEventStore: EventStore, // for if constraint is an even group ID
+  businessHoursUnexpanded: EventStore, // for if constraint is 'businessHours'
+  calendar: Calendar // for expanding businesshours
+): OpenDateRange[] {
 
   if (constraint === 'businessHours') {
-    let store = getPeerBusinessHours(subjectSpan, calendar)
-    store = expandRecurring(store, subjectSpan.range, calendar)
-    return eventStoreToDateSpans(store)
+    return eventStoreToRanges(
+      expandRecurring(businessHoursUnexpanded, subjectRange, calendar)
+    )
 
-  } else if (typeof constraint === 'string') { // an ID
-    let store = filterEventStoreDefs(calendar.state.eventStore, function(eventDef) {
-      return eventDef.groupId === constraint
-    })
-    return eventStoreToDateSpans(store)
+  } else if (typeof constraint === 'string') { // an group ID
+    return eventStoreToRanges(
+      filterEventStoreDefs(otherEventStore, function(eventDef) {
+        return eventDef.groupId === constraint
+      })
+    )
 
   } else if (typeof constraint === 'object' && constraint) { // non-null object
 
     if ((constraint as EventTuple).def) { // an event definition (actually, a tuple)
-      let store = eventTupleToStore(constraint as EventTuple)
-      store = expandRecurring(store, subjectSpan.range, calendar)
-      return eventStoreToDateSpans(store)
+      return eventStoreToRanges(
+        expandRecurring(eventTupleToStore(constraint as EventTuple), subjectRange, calendar)
+      )
 
     } else {
-      return [ constraint as OpenDateSpan ] // already parsed datespan
+      return [ (constraint as OpenDateSpan).range ] // an already-parsed datespan
     }
-
   }
 
   return []
 }
 
-function isOverlapValid(stillEvent: EventTuple, movingEvent: EventTuple | null, overlap: Overlap | null, calendar: Calendar): boolean {
-  if (typeof overlap === 'boolean') {
-    return overlap
-  } else if (typeof overlap === 'function') {
-    return Boolean(
-      overlap(
-        new EventApi(calendar, stillEvent.def, stillEvent.instance),
-        movingEvent ? new EventApi(calendar, movingEvent.def, movingEvent.instance) : null
-      )
-    )
+// TODO: move to event-store file?
+function eventStoreToRanges(eventStore: EventStore): DateRange[] {
+  let { instances } = eventStore
+  let ranges: DateRange[] = []
+
+  for (let instanceId in instances) {
+    ranges.push(instances[instanceId].range)
+  }
+
+  return ranges
+}
+
+// TODO: move to geom file?
+function anyRangesContainRange(outerRanges: DateRange[], innerRange: DateRange): boolean {
+
+  for (let outerRange of outerRanges) {
+    if (rangeContainsRange(outerRange, innerRange)) {
+      return true
+    }
+  }
+
+  return false
+}
+
+
+// Overlap Utils
+// ------------------------------------------------------------------------------------------------------------------------
+
+function allOverlapsPass(
+  overlaps: Overlap[],
+  subjectDef: EventDef | null,
+  subjectInstance: EventInstance | null,
+  otherDef: EventDef,
+  otherInstance: EventInstance,
+  calendar: Calendar
+) {
+  for (let overlap of overlaps) {
+    if (!overlapPasses(overlap, subjectDef, subjectInstance, otherDef, otherInstance, calendar)) {
+      return false
+    }
   }
 
   return true
 }
 
-function isDateSpanAllowed(dateSpan: DateSpan, moving: EventTuple | null, allow: Allow | null, calendar: Calendar): boolean {
-  if (typeof allow === 'function') {
-    return Boolean(
-      allow(
-        buildDateSpanApi(dateSpan, calendar.dateEnv),
-        moving ? new EventApi(calendar, moving.def, moving.instance) : null
-      )
+function overlapPasses(
+  overlap: Overlap,
+  subjectDef: EventDef | null,
+  subjectInstance: EventInstance | null,
+  otherDef: EventDef,
+  otherInstance: EventInstance,
+  calendar: Calendar
+) {
+  if (overlap === false) {
+    return false
+  } else if (typeof overlap === 'function') {
+    return !overlap(
+      new EventApi(calendar, otherDef, otherInstance),
+      subjectDef ? new EventApi(calendar, subjectDef, subjectInstance) : null
     )
   }
 
   return true
 }
 
-function dateSpansCollide(span0: DateSpan, span1: DateSpan): boolean {
-  return rangesIntersect(span0.range, span1.range) && isSpanPropsEqual(span0, span1)
-}
 
-function dateSpanContainsOther(outerSpan: DateSpan, subjectSpan: DateSpan): boolean {
-  return rangeContainsRange(outerSpan.range, subjectSpan.range) &&
-    isSpanPropsMatching(subjectSpan, outerSpan) // subjectSpan has all the props that outerSpan has?
-}
+// Splitting Utils
+// ------------------------------------------------------------------------------------------------------------------------
 
-function eventStoreToDateSpans(store: EventStore): DateSpan[] {
-  return mapEventInstances(store, function(instance: EventInstance, def: EventDef) {
-    return eventToDateSpan(def, instance)
-  })
+interface MinimalSplittableProps {
+  dateSelection?: DateSpan
+  businessHours?: EventStore
+  eventStore?: EventStore
+  eventUiBases?: EventUiHash
 }
 
-// TODO: plugin
-export function eventToDateSpan(def: EventDef, instance: EventInstance): DateSpan {
-  return {
-    allDay: def.allDay,
-    range: instance.range
+function splitMinimalProps(
+  inputProps: MinimalSplittableProps,
+  splitterMeta: ValidationSplitterMeta | null
+): { [key: string]: MinimalSplittableProps } {
+
+  if (splitterMeta) {
+    let splitter = new splitterMeta.splitterClass()
+
+    return splitter.splitProps({
+      businessHours: inputProps.businessHours || createEmptyEventStore(),
+      dateSelection: inputProps.dateSelection || null,
+      eventStore: inputProps.eventStore || createEmptyEventStore(),
+      eventUiBases: inputProps.eventUiBases || {},
+      eventSelection: '',
+      eventDrag: null,
+      eventResize: null
+    })
+  } else {
+    return { '': inputProps }
   }
 }
 
-// TODO: plugin
-function getPeerBusinessHours(subjectSpan: DateSpan, calendar: Calendar): EventStore {
-  return calendar.component.view.props.businessHours // accessing view :(
-}
+
+// Parsing
+// ------------------------------------------------------------------------------------------------------------------------
 
 export function normalizeConstraint(input: ConstraintInput, calendar: Calendar): Constraint | null {
   if (typeof input === 'object' && input) { // non-null object