Пост написан на основе статьи на хабре: Интервально-ассоциативный массив.

Поскольку изначальная реализация основана на слайсах (срезах) питона, нелишней для прочтения будет статья: Всё, что Вы хотели знать о слайсах. И, конечно, немного теории: Дерево Интервалов (Отрезков).
Итак, как же слайсы будут выглядеть в Cache?

В целом всё (почти) как в питоне.

Легко назначить:

i=##class(test.intervalmap).%New()
i.%("08:00","12:00","Иванов")
i.%("12:00","16:00","Петров")
Как узнать кто дежурил в 13:51 ?

i.%Get("13:51"),!
Петров
Легко просмотреть поэлементно полный список (для тех, кто привык мыслить "глобально"):

%=i.% zw %
%("08:00","12:00")="Иванов"
%("12:00","16:00")="Петров"

… или одной строкой:

i.Display(),!
[08:00, 12:00] => Иванов, [12:00, 16:00] => Петров
Удаление как по частям:

i.%("15:00","16:00")
i.Display(),!
[08:00, 12:00] => Иванов, [12:00, 15:00] => Петров
… так и целиком:

i.%("12:00","16:00")
i.Display(),!
[08:00, 12:00] => Иванов
Перекрывание ключей должно обрабатываться корректно:

i.%("11:00","15:00","Сидоров")
i.Display(),!
[08:00, 11:00] => Иванов, [11:00, 15:00] => Сидоров
Cоседние ключи с одинаковыми значениями должны склеиваться автоматически. Например, если вы назначили Сидорову подежурить так же с 15 до 17, то навряд ли это должно быть две смены подряд, скорее — одна более длинная:

i.%("15:00","17:00","Сидоров")
i.Display(),!
[08:00, 11:00] => Иванов, [11:00, 17:00] => Сидоров
Добавим пару записей:

i.%("17:00","20:00","Петров")
i.%("21:00","23:00","Сидоров")
i.Display(),!
[08:00, 11:00] => Иванов, [11:00, 17:00] => Сидоров, [17:00, 20:00] => Петров, [21:00, 23:00] => Сидоров
Часто возникает задача урезать расписание, оставив из нескольких идущих подряд элементов только последние. Например, нужно узнать, кто закрывал рабочий день:

i.Shrink()
i.Display(),!
[08:00, 20:00] => Петров, [21:00, 23:00] => Сидоров
Этот же метод можно использовать для проверки полностью ли ваше расписание охватывает рабочий день.

Дополнение
  • Была учтена небольшая ошибка в исходном коде по склейке соседних ключей, а именно:

    Питон:
    >>> timetable = intervalmap()
    >>> timetable[:] = 'Иванов'
    >>> timetable['11:00':'13:00'] = 'Иванов'
    >>> print timetable
    {[None, '13:00'] => 'Иванов', ['13:00', None] => 'Иванов'}

    COS:

    i=##class(test.intervalmap).%New()
    i.%(,,"Иванов")
    i.%("11:00","13:00","Иванов")
    i.Display(),!
    [None, None] => Иванов
  • Одинаковые ключи в паре, как в исходном коде, здесь не допускаются, но вы для себя можете включить их обратно.
    Конечно же в качестве ключей могут выступать любые числовые/строковые значения. Единственное, нужно следить, чтобы все они в рамках одного массива (объекта) были однотипны.
    Например:

    i=..%New()
    i.%(9,,"!")
    i.%(,5,"Hello")
    i.%(6,7,"World")
    i.Display(),!
    [None, 5] => Hello, [6, 7] => World, [9, None] => !

    i.Reset()
    i.%(,$zdh("24.10.2005"),"A")
    i.%($zdh("11.11.2005"),$zdh("17.11.2005"),"B")
    i.%($zdh("30.11.2005"),,"C")
    i.Display(),!
    [None, 60197] => A, [60215, 60221] => B, [60234, None] => C
    И напоследок ещё один пример посложнее:

    i.Reset()
    i.%(,,$c(8734))
    i.%(10,11,"Иванов")
    i.%(12,13,"Иванов")
    i.%(14,16,"Петров")
    i.%(11,15,"Иванов")
    i.%(8,12,"Сидоров")
    i.%(20,21)
    i.%(22,,"Сидоров")
    i.Display(),!
    Результат под спойлером
    [None, 8] => ?, [8, 12] => Сидоров, [12, 15] => Иванов, [15, 16] => Петров, [16, 20] => ?, [21, 22] => ?, [22, None] => Сидоров

    Больше примеров вы найдёте в исходном коде.

    Код класса

    Class test.intervalmap Extends %RegisteredObject Final ]
    {
    Parameter None [ FinalInternal ] = -1;
    Property bounds As %List InternalPrivateReadOnlyServerOnly = 1, Transient ];
    Property items As %List InternalPrivateReadOnlyServerOnly = 1, Transient ];
    Property upperItem [ InitialExpression = {..#None}, InternalPrivateReadOnlyServerOnly = 1, Transient ];
    Property % [ MultiDimensionalReadOnlyServerOnly = 1, Transient ];
    ClassMethod Slice(list As %Liststartendvalue As %ListAs %List InternalPrivate ]
    {
        
    if start=end {
            
    if start="" {
                
    set list=""
            
    }elseif start=1 {
                
    set list=value_list
            
    }else{
                
    if start>$listlength(list{
                    
    set list=list_value
                
    }else{
                    
    set:start<$listlength(liststart=start+1
                    
    set $list(list,start,start)=value_$listbuild($list(list,start))
                
    }
            }
        }
    else{
            
    if end="" {
                
    set:start>$listlength(liststart=$listlength(list)
                
    set $list(list,start,*+1)=value
            
    }else{
                
    set start=$get(start,1)
                
    if end=1 {
                    
    set list=..Slice(list,start,end,value)
                
    }else{
                    
    set $list(list,start,end-1)=value
                
    }
            }
        }
        
    quit list
    }
    Method Reset()
    {
        
    kill i%%
        
    set (i%bounds,i%items)="",i%upperItem=..#None
    }
    Method %(start = {$get(start,..#None)}, stop = {$get(stop,..#None)}, value = {$get(value,..#None)}) As %Status
    {
        
    quit:(start>=stop)&((start'=..#None)&(stop'=..#None)) $$$OK
        
        set 
    startPoint=$select(start=..#None:0,1:..bisectLeft(start))
        
    set endPoint=$select(stop=..#None:0,1:..bisectLeft(stop))
        
        
    if startPoint>=1 {
            
    set:(startPoint <= $listlength(..bounds))&&($list(..bounds,startPoint)<startstartPoint startPoint + 1
          
    set:(endPoint >= 1)&&(endPoint <= $listlength(..bounds))&&($list(..bounds,endPoint)<=stopendPoint endPoint + 1
          
        
    if endPoint>=1 {
            
    set i%bounds=..Slice(i%bounds,startPoint,endPoint,$listbuild(start,stop))
            
    set i%items=..Slice(i%items,startPoint,endPoint,$select(startPoint <= $listlength(..items):$listbuild($list(..items,startPoint),value),1:$listbuild(..upperItem,value)))
        
    }else{
          
    set $list(i%bounds,startPoint,*+1) = $listbuild(start)
          
    set $list(i%items,startPoint,*+1)=$select(startPoint <= $listlength(..items):$listbuild($list(..items,startPoint),value),1:$listbuild(..upperItem))
          
    set i%upperItem = value
        
    }
        }
    else{
        
    if endPoint>=1 {
            
    set i%bounds=..Slice(i%bounds,1,endPoint,$listbuild(stop))
            
    set i%items=..Slice(i%items,1,endPoint,$listbuild(value))
        
    }else{
          
    set (i%bounds,i%items) = ""
          
    set i%upperItem = value
        
    }
        }
        
    set i=1
        
    while (i<=($listlength(..items)-1))
        
    {
          
    if $list(..items,i)=$list(..items,i+1) {
            
    set $list(i%items,i,i)=""
            
    set $list(i%bounds,i,i)=""
          
    }else {
              
    set i=i+1
          
    }
      }
      
    set:($listlength(..items)=1)&&($list(i%items,1)=i%upperItem) (i%items,i%bounds)=""
              
      
    do ..repr()
      
      
    quit $$$OK
    }
    Method %Get(xAs %String ServerOnly = 1 ]
    {
        
    set index=..bisectRight(x)
        
    set r=$select(index<=$listlength(i%items):$list(i%items,index),1:i%upperItem)
        
    quit $select(r=..#None:"",1:r)
    }
    Method bisectLeft(xAs %String InternalPrivateServerOnly = 1 ]
    {
        
    set lo = 1
        
    set hi $listlength(i%bounds)+1
        
    while (lo hi{
        
    set mid = (lo+hi)\2
        
    if $list(i%bounds,mid) < {
            
    set lo mid+1
        
    else {
            
    set hi mid
        
    }
        }
        
    quit lo
    }
    Method bisectRight(xAs %String InternalPrivateServerOnly = 1 ]
    {
        
    set lo = 1
        
    set hi $listlength(i%bounds)+1
        
    while (lo hi{
        
    set mid = (lo+hi)\2
        
    if $list(i%bounds,mid{
            
    set hi mid
        
    else {
            
    set lo mid+1
        
    }
        }
        
    quit lo
    }
    Method repr() [ InternalPrivateServerOnly = 1 ]
    {
      
    kill i%%
      
    set previousBound=..#None
      for 
    i=1:1:$listlength(..bounds{
          
    set b=$list(..bounds,i)
          
    set v=$list(..items,i)
          
    set:v'=..#None i%%(previousBound,b)=v
          
    set previousBound=b
      
    }
      
    set:..upperItem'=..#None i%%(previousBound,..#None)=..upperItem
    }
    Method Shrink()
    {
        
    set i=1
        
    while (i<=($listlength(..items)-1))
        
    {
          
    if $list(..items,i)'=..#None,$list(..items,i+1)'=..#None {
            
    set $list(i%items,i,i)=""
            
    set $list(i%bounds,i,i)=""
          
    }else {
              
    set i=i+1
          
    }
      }
      
    do ..repr()
    }
    Method Display() As %String
    {
        
    #define IsNone(%s) $s(%s=..#None:"None",1:%s)
        
        set 
    key=$query(i%%,1,v),s=""
        
    while (key'=""{
            
    set s=s_$listbuild($$$FormatText("[%1, %2] => %3",$$$IsNone($qsubscript(key,1)),$$$IsNone($qsubscript(key,2)),v))
            
    set key $query(@key,1,v)
        
    }
        
    quit $listtostring(s,", ")
    }
    /// <example>d ##class(test.intervalmap).Test1()</example>
    ClassMethod 
    Test1() [ InternalServerOnly = 1 ]
    {
        
    new %
        
    set old=$system.Process.Undefined(2)
        
        
    try{
            
    set i=..%New()
            
    do i.%("08:00","12:00","Иванов")
            
    do i.%("12:00","16:00","Петров")
            
    do i.%("15:00","16:00")
            
    do i.%("12:00","16:00")
            
    do i.%("11:00","15:00","Сидоров")
            
    do i.%("15:00","17:00","Сидоров")
            
    do i.%("17:00","20:00","Петров")
            
    do i.%("21:00","23:00","Сидоров")
            
    write i.Display(),!
            
    write "[13:51] = ",i.%Get("13:51"),!
            
    ;k % m %=i.% zw %
            
    do i.Shrink()
            
    write i.Display(),!
        
    }catch(ex){
            
    #dim ex As %Exception.AbstractException
            
    write "Error = ",ex.DisplayString(),!
        
    }
        
    do $system.Process.Undefined(old)
    }
    /// <example>d ##class(test.intervalmap).Test2()</example>
    ClassMethod 
    Test2() [ InternalServerOnly = 1 ]
    {
        
    #define Assert(%i,%s) if %i.Display()'=%s {$$$ThrowStatus($$$ERROR($$$GeneralError,%s))} else {w %i.Display(),!}
        #define 
    AssertGet(%i,%t,%s) if %i.%Get(%t)'=%s {$$$ThrowStatus($$$ERROR($$$GeneralError,%s))} else {w "(%t) = ",%i.%Get(%t),!}
        set 
    old=$system.Process.Undefined(2)
        
        
    try{
            
            
    set i=..%New()
            
    do i.%(0,5,"0-5")
            
    do i.%(8,12,"8-12")
            
    $$$AssertGet(i,2,"0-5")
            
    $$$AssertGet(i,10,"8-12")
            
    $$$AssertGet(i,-1,"")
            
    $$$AssertGet(i,17,"")
            
            
    do i.%(4,9,"4-9")
            
    $$$Assert(i,"[0, 4] => 0-5, [4, 9] => 4-9, [9, 12] => 8-12")
            
    do i.%(,0,"less than 0")
            
    $$$AssertGet(i,-5,"less than 0")
            
    $$$AssertGet(i,0,"0-5")
            
    $$$Assert(i,"[None, 0] => less than 0, [0, 4] => 0-5, [4, 9] => 4-9, [9, 12] => 8-12")
            
            
    do i.%(21,,"more than twenty")
            
    $$$AssertGet(i,42,"more than twenty")
            
    do i.%(10.5,15.5,"10.5-15.5")
            
    $$$AssertGet(i,11.5,"10.5-15.5")
            
    $$$AssertGet(i,0.5,"0-5")
            
    $$$Assert(i,"[None, 0] => less than 0, [0, 4] => 0-5, [4, 9] => 4-9, [9, 10.5] => 8-12, [10.5, 15.5] => 10.5-15.5, [21, None] => more than twenty")
            
            
    do i.Reset()
            
            
    do i.%(0,2,1)
            
    do i.%(2,8,2)
            
    do i.%(4,,3)
            
    do i.%(5,6,4)
            
    $$$Assert(i,"[0, 2] => 1, [2, 4] => 2, [4, 5] => 3, [5, 6] => 4, [6, None] => 3")
            
        
    }catch(ex){
            
    #dim ex As %Exception.AbstractException
            
    write "Error = ",ex.DisplayString(),!
        
    }
        
    do $system.Process.Undefined(old)
    }
    /// <example>d ##class(test.intervalmap).Test3()</example>
    ClassMethod 
    Test3() [ InternalServerOnly = 1 ]
    {
        
    #define Assert(%i,%s) if %i.Display()'=%s $$$ThrowStatus($$$ERROR($$$GeneralError,%s))
        
    #define AssertGet(%i,%t,%s) if %i.%Get(%t)'=%s $$$ThrowStatus($$$ERROR($$$GeneralError,%s))
        
    set old=$system.Process.Undefined(2)
        
        
    try{
            
            
    set i=..%New()
            
    do i.%(9,,"!")
            
    $$$Assert(i,"[9, None] => !")
            
    do i.%(,5,"Hello")
            
    do i.%(6,7,"World")
            
    $$$Assert(i,"[None, 5] => Hello, [6, 7] => World, [9, None] => !")
            
    do i.%(8,10,"(Test)")
            
    $$$Assert(i,"[None, 5] => Hello, [6, 7] => World, [8, 10] => (Test), [10, None] => !")
            
    do i.%(,3,"My,")
            
    $$$Assert(i,"[None, 3] => My,, [3, 5] => Hello, [6, 7] => World, [8, 10] => (Test), [10, None] => !")
            
    do i.%(5.5,6,"Cruel")
            
    $$$Assert(i,"[None, 3] => My,, [3, 5] => Hello, [5.5, 6] => Cruel, [6, 7] => World, [8, 10] => (Test), [10, None] => !")
            
    do i.%(6,6.5,"And Harsh")
            
    $$$Assert(i,"[None, 3] => My,, [3, 5] => Hello, [5.5, 6] => Cruel, [6, 6.5] => And Harsh, [6.5, 7] => World, [8, 10] => (Test), [10, None] => !")
            
    do i.%(5.9,6.6)
            
    $$$Assert(i,"[None, 3] => My,, [3, 5] => Hello, [5.5, 5.9] => Cruel, [6.6, 7] => World, [8, 10] => (Test), [10, None] => !")
            
    write "Test 1 OK",!
            
            
    do i.Reset()
            
    do i.%(,0,"A")
            
    do i.%(2,5,"B")
            
    do i.%(8,10,"C")
            
    do i.%(12,,"D")
            
    $$$Assert(i,"[None, 0] => A, [2, 5] => B, [8, 10] => C, [12, None] => D")
            
    do i.%(,,"K")
            
    $$$Assert(i,"[None, None] => K")
            
    $$$AssertGet(i,5,"K")
            
    do i.%(0,10,"L")
            
    do i.%(6,8,"M")
            
    do i.%(20,,"J")
            
    $$$AssertGet(i,-1,"K")
            
    $$$AssertGet(i,5,"L")
            
    $$$AssertGet(i,7,"M")
            
    $$$AssertGet(i,9,"L")
            
    $$$AssertGet(i,15,"K")
            
    write "Test 2 OK",!
            
            
    do i.Reset()
            
    do i.%(,$zdateh("24.10.2005"),"A")
            
    do i.%($zdateh("11.11.2005"),$zdateh("17.11.2005"),"B")
            
    do i.%($zdateh("30.11.2005"),,"C")
            
    $$$AssertGet(i,$zdateh("25.09.2005"),"A")
            
    $$$AssertGet(i,$zdateh("23.10.2005"),"A")
            
    $$$AssertGet(i,$zdateh("26.10.2005"),"")
            
    $$$AssertGet(i,$zdateh("09.11.2005"),"")
            
    $$$AssertGet(i,$zdateh("16.11.2005"),"B")
            
    $$$AssertGet(i,$zdateh("23.11.2005"),"")
            
    $$$AssertGet(i,$zdateh("29.11.2005"),"")
            
    $$$AssertGet(i,$zdateh("30.11.2005"),"C")
            
    $$$AssertGet(i,$zdateh("03.12.2005"),"C")
            
    write "Test 3 OK",!
            
        
    }catch(ex){
            
    #dim ex As %Exception.AbstractException
            
    write "Error = ",ex.DisplayString(),!
        
    }
        
    do $system.Process.Undefined(old)
    }
    }


    Или скачать класс test.intervalmap.
    Код тестировался на версии Cache 2015.1, но переделать класс для предыдущих версий не составит особого труда.

Комментарии ()