Campos de data no MySQL, encontrando todas as linhas que não se sobrepõem e retornando apenas a diferença

StackOverflow https://stackoverflow.com/questions/1333340

Pergunta

Essa foi uma das minhas primeiras perguntas aqui, mas tenho uma pequena variação:

Então, tenho duas pessoas cujas agendas estão em um banco de dados.As programações simplesmente registram a hora de início, a hora de término e a descrição dos vários eventos/compromissos para ambos os usuários.

PessoaA deseja negociar compromissos com PessoaB.Eu quero uma consulta MySQL que retorne todas as vezes que PersonB e PersonA podem trocar.

Originalmente, os parâmetros da consulta deveriam eliminar quaisquer compromissos da PessoaB onde houvesse sobreposição com a PessoaA e o compromisso da PessoaB deveria ter exatamente o mesmo comprimento que o compromisso que a PessoaA deseja trocar.Recebi ótimos conselhos sobre aritmética/geometria do tempo que me ajudaram a obter os resultados que precisava.

Agora quero alterar o parâmetro 1 para 1 para que os compromissos não precisem ter a mesma duração.Portanto, se PersonA quiser trocar seu compromisso de segunda-feira de manhã (10h00 - 11h30), a consulta irá:

  • Exclua qualquer compromisso da Pessoa B que ocorra durante um dos compromissos da Pessoa A
  • Incluir qualquer compromisso da Pessoa B que esteja fora dos compromissos da Pessoa A
  • Inclua as partes dos compromissos da PessoaB que estão enquanto a PessoaA é gratuita, mas mostre apenas a parte gratuita.

Portanto, se a PessoaA quiser trocar o compromisso acima (novamente, segunda-feira, das 10h00 às 11h30), e a PessoaA tiver um compromisso na terça-feira, das 13h00 às 15h00, e a PessoaB tiver um compromisso na terça-feira, a partir das 12h: Das 00h às 16h, a consulta retornaria:

Possible_Swaps
==============
userID  | Start             | End             | Description
PersonB | Tuesday, 12:00 PM | Tuesday 1:00 PM | Cooking
PersonB | Tuesday,  4:00 PM | Tuesday 5:00 PM | Cooking

Além de quaisquer outras possibilidades.Isso é esperar demais do banco de dados?Em caso afirmativo, alguma sugestão sobre como pelo menos obter essas mudanças que estão sobrepostas, mas que têm os tempos pendentes de ambos os lados para que um script PHP possa lidar com elas?


a pedido de searlea, aqui está um pouco mais de contexto:

Continuei dizendo compromissos, mas acho que realmente quis dizer "empregos" como "turnos de trabalho".PersonA e PersonB trabalham no mesmo escritório.No vcalendar, os turnos de trabalho são geralmente chamados de "Eventos", mas ocasionalmente "Compromissos" e eu optei pelo último, pois parece menos que as duas pessoas vão a uma feira.

Portanto, PersonA tem turno de lavagem de louça na segunda-feira, das 10h00 às 11h30.PersonB está cozinhando na terça-feira, das 12h00 às 17h00.PersonA realmente quer ver seu irmão antes de partirem da cidade na segunda-feira.Ele preferiria tirar toda a manhã de segunda-feira de folga, mas se contentaria em tirar uma hora de folga.

Então, no meu modelo antigo (criado em minha primeira pergunta aqui), eu estava procurando por turnos onde não houvesse sobreposição e onde os turnos fossem iguais no tempo.Mas isso tem dois problemas:

  1. Se eu precisar de alguém para cobrir meu turno de 2 horas na terça e eu trabalhar 4 horas na quinta, e Joe trabalhar 8 horas na quinta, eu poderia trocar duas de suas horas e ele poderia sair um pouco mais cedo e eu poderia ficar um pouco mais tarde.

  2. Se eu tenho um turno de duas horas, mas ficaria feliz em trocar uma hora apenas para chegar ao aeroporto a tempo, quero saber se tal e tal chega uma hora antes de mim no final da semana para que eu possa pegue essa parte do turno dele.

Para encurtar a história (tarde demais), quero o que aparentemente é conhecido como complemento relativo dos turnos da PessoaA para a PessoaB (basicamente sempre que a PessoaB está trabalhando e a PessoaA não, independentemente de os turnos se sobreporem em algum outro ponto).

Idealmente, eu obteria um conjunto de resultados que incluísse os bits em que a PessoaB estava trabalhando e a PessoaA não (os dois turnos de 1 hora mencionados acima), bem como o turno inteiro (com uma tag especial para indicar que não está disponível como um todo) para que a Pessoa A visse que ele estava cobrindo parte de um turno e não ficasse confusa e pensasse que a Pessoa B estava trabalhando em dois turnos de uma hora.

Tudo isso está começando a parecer um pouco complicado.Basicamente, quero que os turnos da PessoaB sejam azuis, os turnos da PessoaA sejam amarelos e quero que o banco de dados retorne todas as partes que não são verdes.

Foi útil?

Solução

SELECT * 
  FROM schedule AS s1
WHERE
  s1.user = 'Ondra'
AND
NOT EXISTS ( 
  SELECT * FROM schedule AS s2 
  WHERE
    s2.user = 'Zizka'
    AND (
      s2.start BETWEEN s1.start AND s1.end 
      OR
      s2.end BETWEEN s1.start AND s1.end 
      OR 
      s1.start > s2.start AND s1.end < s2.end 
    )
)

Isso seleciona os eventos de Ondra que podem se encaixar em uma lacuna no diário de Zizka.

Editado: Originalmente, era um intersect, mas se você deseja o complemento relativo, isso é suficiente.

Outras dicas

Deixar $shift_id Seja o ID da mudança que seu usuário deseja trocar.

select swappable.shift_id, swappable.user_id, swappable.description,
    FROM_UNIXTIME(swappable.shiftstart) as start,
    FROM_UNIXTIME(swappable.shiftend) as end,
    (swappable.shiftend - swappable.shiftstart) -
        sum(coalesce(least(conflict.shiftend, swappable.shiftend) -
            greatest(conflict.shiftstart, swappable.shiftstart), 0))
        as swaptime,
    group_concat(conflict.shift_id) as conflicts,
    group_concat(concat(FROM_UNIXTIME(conflict.shiftstart), ' - ',
        FROM_UNIXTIME(conflict.shiftend))) as conflict_times
from shifts as problem
join shifts as swappable on swappable.user_id != problem.user_id
left join shifts as conflict on conflict.user_id = problem.user_id
    and conflict.shiftstart < swappable.shiftend
    and conflict.shiftend > swappable.shiftstart
where problem.shift_id = 1
group by swappable.shift_id
having swaptime > 0;

Testado com:

CREATE TABLE `shifts` (
  `shift_id` int(10) unsigned NOT NULL auto_increment,
  `user_id` varchar(20) NOT NULL,
  `shiftstart` int unsigned NOT NULL,
  `shiftend` int unsigned NOT NULL,
  `description` varchar(32) default NULL,
  PRIMARY KEY  (`shift_id`)
);

insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (1,'april', UNIX_TIMESTAMP('2009-04-04 10:00:00'),UNIX_TIMESTAMP('2009-04-04 12:00:00'),'Needs to be swapped');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (2,'bill',  UNIX_TIMESTAMP('2009-04-04 10:30:00'),UNIX_TIMESTAMP('2009-04-04 11:30:00'),'Inside today');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (3,'casey', UNIX_TIMESTAMP('2009-04-04 12:00:00'),UNIX_TIMESTAMP('2009-04-04 14:00:00'),'Immediately after today');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (4,'casey', UNIX_TIMESTAMP('2009-04-04 08:00:00'),UNIX_TIMESTAMP('2009-04-04 10:00:00'),'Immediately before today');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (5,'david', UNIX_TIMESTAMP('2009-04-04 11:00:00'),UNIX_TIMESTAMP('2009-04-04 15:00:00'),'Partly after today');

insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (6,'april', UNIX_TIMESTAMP('2009-04-05 10:00:00'),UNIX_TIMESTAMP('2009-04-05 12:00:00'),'Tommorow');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (7,'bill',  UNIX_TIMESTAMP('2009-04-05 09:00:00'),UNIX_TIMESTAMP('2009-04-05 11:00:00'),'Partly before tomorrow');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (8,'casey', UNIX_TIMESTAMP('2009-04-05 10:00:00'),UNIX_TIMESTAMP('2009-04-05 12:00:00'),'Equals tomorrow');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (9,'david', UNIX_TIMESTAMP('2009-04-05 10:30:00'),UNIX_TIMESTAMP('2009-04-05 11:30:00'),'Inside tomorrow');

insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (10,'april',UNIX_TIMESTAMP('2009-04-11 10:00:00'),UNIX_TIMESTAMP('2009-04-11 12:00:00'),'Next week');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (11,'april',UNIX_TIMESTAMP('2009-04-11 12:00:00'),UNIX_TIMESTAMP('2009-04-11 14:00:00'),'Second shift');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (12,'bill', UNIX_TIMESTAMP('2009-04-11 11:00:00'),UNIX_TIMESTAMP('2009-04-11 13:00:00'),'Overlaps two');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (13,'casey',UNIX_TIMESTAMP('2009-04-11 17:00:00'),UNIX_TIMESTAMP('2009-04-11 19:00:00'),'No conflict');

insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (14,'april',UNIX_TIMESTAMP('2009-05-04 10:00:00'),UNIX_TIMESTAMP('2009-05-04 12:00:00'),'Next month');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (15,'april',UNIX_TIMESTAMP('2009-05-04 13:00:00'),UNIX_TIMESTAMP('2009-05-04 15:00:00'),'After break');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (16,'bill', UNIX_TIMESTAMP('2009-05-04 11:00:00'),UNIX_TIMESTAMP('2009-05-04 14:00:00'),'Middle okay');

insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (17,'april',UNIX_TIMESTAMP('2010-04-04 10:00:00'),UNIX_TIMESTAMP('2010-04-04 11:00:00'),'Next year');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (18,'april',UNIX_TIMESTAMP('2010-04-04 11:30:00'),UNIX_TIMESTAMP('2010-04-04 12:00:00'),'After break');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (19,'april',UNIX_TIMESTAMP('2010-04-04 12:30:00'),UNIX_TIMESTAMP('2010-04-04 13:30:00'),'Third part');
insert  into `shifts`(`shift_id`,`user_id`,`shiftstart`,`shiftend`,`description`) values (20,'bill', UNIX_TIMESTAMP('2010-04-04 10:30:00'),UNIX_TIMESTAMP('2010-04-04 13:00:00'),'Two parts okay');

Resultados:

'shift_id', 'user_id', 'description',              'start',               'end',                 'swaptime', 'conflicts', 'conflict_times'
 '3',       'casey',   'Immediately after today',  '2009-04-04 12:00:00', '2009-04-04 14:00:00', '7200',       NULL,       NULL
 '4',       'casey',   'Immediately before today', '2009-04-04 08:00:00', '2009-04-04 10:00:00', '7200',       NULL,       NULL
 '5',       'david',   'Partly after today',       '2009-04-04 11:00:00', '2009-04-04 15:00:00', '10800',     '1',        '2009-04-04 10:00:00 - 2009-04-04 12:00:00'
 '7',       'bill',    'Partly before tomorrow',   '2009-04-05 09:00:00', '2009-04-05 11:00:00', '3600',      '6',        '2009-04-05 10:00:00 - 2009-04-05 12:00:00'
'13',       'casey',   'No conflict',              '2009-04-11 17:00:00', '2009-04-11 19:00:00', '7200',       NULL,       NULL
'16',       'bill',    'Middle okay',              '2009-05-04 11:00:00', '2009-05-04 14:00:00', '3600',      '15,14',    '2009-05-04 13:00:00 - 2009-05-04 15:00:00,2009-05-04 10:00:00 - 2009-05-04 12:00:00'
'20',       'bill',    'Two parts okay',           '2010-04-04 10:30:00', '2010-04-04 13:00:00', '3600',      '19,18,17', '2010-04-04 12:30:00 - 2010-04-04 13:30:00,2010-04-04 11:30:00 - 2010-04-04 12:00:00,2010-04-04 10:00:00 - 2010-04-04 11:00:00'

Isso mostra todas as mudanças pelas quais qualquer porção (s) pode ser trocada, incluindo quanto tempo total (em segundos) é trocável. A coluna final, conflict_times, mostra os horários para os quais o usuário de troca já está programado para funcionar. Deve ser fácil para o aplicativo extrair os horários disponíveis; É possível, mas muito complicado, no MySQL.

Tarefa

Retorne todos os intervalos de dois usuários diferentes, exceto as partes onde eles se sobrepõem.

Tabela e dados de teste

CREATE TABLE IF NOT EXISTS `shifts` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(1) NOT NULL,
  `start` datetime NOT NULL,
  `end` datetime NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=12 ;

INSERT INTO `shifts` (`id`, `name`, `start`, `end`) VALUES
(1, 'a', '2000-01-01 01:00:00', '2000-01-01 03:00:00'),
(2, 'a', '2000-01-01 06:00:00', '2000-01-01 07:30:00'),
(3, 'b', '2000-01-01 02:00:00', '2000-01-01 04:00:00'),
(4, 'b', '2000-01-01 05:00:00', '2000-01-01 07:00:00'),
(5, 'a', '2000-01-01 08:00:00', '2000-01-01 11:00:00'),
(6, 'b', '2000-01-01 09:00:00', '2000-01-01 10:00:00'),
(7, 'a', '2000-01-01 12:00:00', '2000-01-01 13:00:00'),
(8, 'b', '2000-01-01 14:00:00', '2000-01-01 14:30:00'),
(9, 'a', '2000-01-01 16:00:00', '2000-01-01 18:00:00'),
(10, 'a', '2000-01-01 19:00:00', '2000-01-01 21:00:00'),
(11, 'b', '2000-01-01 17:00:00', '2000-01-01 20:00:00');

Resultado dos testes

        id  name    start           end
        1   a   2000-01-01 01:00:00 2000-01-01 02:00:00
        3   b   2000-01-01 03:00:00 2000-01-01 04:00:00
        4   b   2000-01-01 05:00:00 2000-01-01 06:00:00
        2   a   2000-01-01 07:00:00 2000-01-01 07:30:00
        5   a   2000-01-01 10:00:00 2000-01-01 11:00:00
        7   a   2000-01-01 12:00:00 2000-01-01 13:00:00
        8   b   2000-01-01 14:00:00 2000-01-01 14:30:00
        9   a   2000-01-01 16:00:00 2000-01-01 17:00:00
        11  b   2000-01-01 18:00:00 2000-01-01 19:00:00
        10  a   2000-01-01 20:00:00 2000-01-01 21:00:00

Solução

Usei um recurso do MySQL chamado Variáveis ​​​​Definidas pelo Usuário para atingir o objetivo com a seguinte consulta:

SET @inA=0, @inB=0, @lastAstart = 0, @lastBstart = 0, @lastAend = 0, @lastBend = 0;
SELECT id,name,start,end FROM (
    SELECT 
        id,name,
        IF(name='a',
          IF(UNIX_TIMESTAMP(start) > @lastBend, start, FROM_UNIXTIME(@lastBend)),
          IF(UNIX_TIMESTAMP(start) > @lastAend, start, FROM_UNIXTIME(@lastAend))
        ) as start,
        IF(name='a',
          IF(@inB,FROM_UNIXTIME(@lastBstart),end),
          IF(@inA,FROM_UNIXTIME(@lastAstart),end)
        )  as end,
        IF(name='a',
          IF(@inB AND (@lastBstart < @lastAstart), 1, 0),
          IF(@inA AND (@lastAstart < @lastBstart), 1, 0)
        ) as fullyEnclosed,
          isStart,
          IF(name='a',@inA:=isStart,0), 
          IF(name='b',@inB:=isStart,0), 
          IF(name='a',IF(isStart,@lastAstart:=t,@lastAend:=t),0), 
          IF(name='b',IF(isStart,@lastBstart:=t,@lastBend:=t),0)
    FROM (
            (SELECT *, UNIX_TIMESTAMP(start) as t, 1 as isStart FROM `shifts` WHERE name IN ('a', 'b'))
        UNION ALL 
            (SELECT *, UNIX_TIMESTAMP(end) as t, 0 as isStart FROM `shifts` WHERE name IN ('a', 'b'))
        ORDER BY t
    ) as sae
) AS final WHERE NOT isStart AND NOT fullyEnclosed;

A ideia básica é listar a tabela duas vezes, ordenada por tempo, para que cada registro apareça duas vezes.Uma vez para a hora de início e depois para a hora de término.Em seguida, estou usando variáveis ​​definidas pelo usuário para acompanhar o estado enquanto percorro os registros e retornar apenas registros de 'hora de término' com hora de início e hora de término ajustadas para intervalos sobrepostos.

Premissas

A única suposição é que nenhum intervalo da pessoa x se sobrepõe a outro intervalo da mesma pessoa.

Comportamento

Alguns casos e seus resultados:

<  (   >   )
<  >   (   )

( < )  ( > )
( ) <  > ( )

<  (   )   >    // for this and similar cases only last part of interval is returned
       <   >

(   <  )   (   )  (  )  (   >   )  // like so
(   )                <  >   (   )

Ressalvas

Devo ter usado o carimbo de data / hora unix, pois meu servidor mysql não conseguiu fazer comparação entre DATETIME mantido na variável definida pelo usuário e outra coisa.

Prós e contras

Ele faz seu trabalho em uma única passagem, sem nenhuma junção, portanto deve levar O(N) tempo.Ele não pode recuperar todas as partes do intervalo da pessoa A cortadas pelos intervalos fechados da pessoa B.Ele usa funcionalidade específica do MySQL.

Para a referência, um código cortado que eu usei recentemente. Pode ser usado para verificar se há intervalos de data sobrepostos. Está escrito em Ruby on Rails, mas a idéia (a declaração SQL) pode ser facilmente traduzida para outros idiomas)

  class Absence
    named_scope :overlaps, lambda { |start, ende| { 
      :conditions =>
          ["   absences.start_date BETWEEN :start AND :end " +
           "OR absences.end_date   BETWEEN :start AND :end " +
           "OR :start BETWEEN absences.start_date AND absences.end_date " +
           "OR :end BETWEEN absences.start_date AND absences.end_date ",
              {:start => start, :end => ende } ]
      }}
  end

Como sempre, com escopos nomeados, esse escopo pode ser reutilizado em combinação com outros escopos.

user = User.find(...)
today = Date.today
confirmed_absences = user.absences.confirmed.overlaps(today.beginning_of_month, today.end_of_month).count
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top