Projeto final de SOP - turmas 2070114 e 2070115

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar

Etapa 1

Período: 23/10 a 06/11

Descrição

Interface com o usuário da agenda, modelagem de eventos e armazenamento da agenda em memória

Interface

A agenda deve apresentar uma interface de texto. Nessa interface o usuário pode escolher uma das seguintes opções, com as respectivas operações a serem desencadeadas:

  1. Criar agenda: criar uma nova agenda, que deve ter um nome único.
  2. Ler agenda: carregar para a memória uma agenda que reside em um arquivo.
  3. Gravar agenda: gravar em um arquivo a agenda que reside em mempória.
  4. Inserir evento: adicionar um novo evento à agenda, que não pode conflitar com o horário de outro evento que já exista.
  5. Mostrar eventos: mostrar em ordem cronológica todos os eventos da agenda.
  6. Remover evento: remover um evento específico.
  7. Sair: sair do programa.

Eventos

Os eventos devem ser modelados para que contenham as seguintes informações:

  • Título
  • Descrição
  • Data e horário de criação
  • Data e horário de início
  • Data e horário de término
  • Categoria
  • Local
  • Status
  • Recorrência

No programa um evento deve ser guardado em uma variável do tipo struct. Uma struct funciona como um registro, em que diferentes valores logicamente relacionados são agrupados e podem ser manipulados usando uma única variável.

23/10: início

Focou-se a criação da interface, que mostra as opções para o usuário e aguarda que ele digite o número da opção escolhida. Para que a interface repita até que o usuário escolha a opção correspodente a sair do programa, usou-se uma estrutura de repetição while. Para o processamento da opção escolhida discutiu-se que a forma mais conveniente é usar a estrutura de decisão switch ... case (equivalente ao Escolhe ... caso do Portugol). Um resumo sobre isto pode ser visto nas transparências.

Usando while e do ... while

No Portugol existe a estrutura de repetição Enquanto condição faz, como mostrado abaixo:

Inicio
  inteiro x

  escrever "Vou mostrar os números pares entre 0 e 100\n"
  x <- 0
  Enquanto x < 101 faz
    escrever x, " "
    x <- x + 2
  fimEnquanto
fim

Em linguagem C há o equivalente while (condição). O exemplo a seguir é o equivalente ao algoritmo em Portugol acima:

int main() {
  int x;

  puts("Vou mostrar os números pares entre 0 e 100");
  x = 0;
  while (x < 101) {
    printf("%d ", x);
    x = x + 2;
  }
}

Além da estrutura while, a linguagem C oferece também do {} while (condição);. Diferente do while (condição) , a condição é avaliada ao final da execução do laço. Assim, com do {} while (condição); as instruções contidas no laço são sempre executadas ao menos uma vez. Isto pode ser útil na criação da interface da agenda. Veja o exemplo abaixo:

int main() {
  int opcao;

  do {
    printf("1. Fazer algo\n");
    printf("2. Sair\n");
    printf("Opção: ");
    scanf("%d", &opcao);

    // aqui se deve testar a opção digitada, e executar a ação correspondente

  } while (opcao != 2);
  printf("Fui !\n");
}

Mais informações podem ser encontradas nestas transparências, e também no curso de linguagem C da UFMG.

Testando diferentes valores de uma variável

A interface da agenda implica a apresentação de um menu de opções, a leitura da opção a ser executada, e em seguida a identificação de qual opção o usuário escolheu. Para saber que opção foi digitada, o programa deve comparar o valor da variável correspondente com os valores das opções disponíveis. Por exemplo, se o usuário digitar 1, então deve-se criar uma nova agenda, se ele digitar 4, deve-se inserir um novo envento na agenda, e se ele digitar 7, deve-se terminar o programa. Uma forma direta de fazer isto é usar a estrutura de decisão switch .. case, que equivale a Escolhe .. caso do Portugol. Veja este exemplo do Portugol:

inicio
  inteiro opcao

  escrever "Opção: "
  ler opcao
  escolhe opcao
    caso 1: 
      escrever "Você escolheu a opção 1\n"
    caso 2: 
      escrever "Você escolheu a opção 2\n"
    defeito:
      escrever "Opção desconhecida !\n"
  fimEscolhe

Fim

O equivalente em linguagem C é:

int main() {
  int opcao;

  printf("Opção: ");
  scanf("%d", &opcao);
  switch (opcao) {
    case 1:
      puts("Você escolheu a opção 1");
      break;
    case 2:
      puts("Você escolheu a opção 2");
      break;
    case 1:
      puts("Opção desconhecida !");
      break;
  }
}

Mais informações podem ser encontradas nestas transparências, e também no curso de linguagem C da UFMG.

O problema do scanf, quando se digita um valor inválido

Algumas equipes identificaram um problema na leitura da opção pelo teclado. Se o usuário digitar algo que não seja um número inteiro, a interface entra em loop, sempre mostrando o menu de opções. Para exemplificar, veja abaixo a leitura de um número inteiro feita com scanf:

  int main() {
    int opcao, lidos;

    do {
      // aqui mostra as opções do menu

      printf("Digite uma opção: ");
      lidos = scanf("%d", &opcao);
      printf("Opção escolhida=%d\n", opcao);

      // executa a ação correspondente à opção escolhida

    } while (opcao != 7); // neste exemplo, a opção 7 faz a saída do programa
  }

Este video mostra o problema do loop quando se digita um valor inválido.

Lembre que o scanf tenta ler o valor digitado de acordo com a especificação contida no formato (no exemplo, "%d" que corresponde a um número inteiro). Se ele conseguir, retorna a quantidade de valores lidos (no exemplo isto é guardado na variável lidos). Assim, se scanf no exemplo acima não conseguir ler o valor digitado, irá retornar 0. O problema é que neste caso o valor que foi digitado fica pendente em memória, quer dizer, o scanf não descarta esse valor, apesar de não ter conseguido lê-lo. Na próxima vez que o scanf for chamado ele vai tentar novamente ler esse mesmo valor, e se der erro o valor vai continuar pendente. Isto só se reverterá quando o scanf especificar um tipo de dado compatível com o valor pendente, ou então se o formato especificar que se deve descartá-lo. Uma possível correção seria portanto testar se o scanf conseguiu ler o valor, e em caso negativo descartar tudo o que foi digitado. Veja assim o exemplo abaixo com a correção:

  int main() {
    int x, n;

    do {
      // aqui mostra as opções do menu

      // o laço abaixo faz com que se repita a leitura pelo teclado até que se digite um
      // número inteiro.
      do {
        printf("Digite uma opção: ");
        n = scanf("%d", &x);

        // se n == 0, então scanf não conseguiu ler o valor, porque ele não é do tipo inteiro.
        // Então o valor digitado deve ser descartado. Para descartar tudo o que foi digitado, deve-se
        // chamar de novo o scanf, mas com o formato "%*[^\n]". Isto joga fora toda a linha digitada.
        // Uma explicação sobre formatos do scanf está na sua página de manual (execute "man scanf").
        if (n == 0) {
          n = scanf("%*[^\n]");
        }
      } while (n == 0);

      printf("Opção escolhida=%d\n", opcao);

      // executa a ação correspondente à opção escolhida

    } while (opcao != 7); // neste exemplo, a opção 7 faz a saída do programa
  }

Com a correção acima, o problema do loop desaparece, o que se pode ver neste outro video.

Esse problema da leitura de valores com scanf pode aparecer em outras partes da agenda. Então seria mais prático se o algoritmo contido no programa acima, que repete a leitura do teclado até que se consiga ler uma valor válido, pudesse ser reaproveitado. Isto é um bom exemplo do uso de funções da linguagem C. Uma função é um algoritmo a ser reutilizado. Como todo algoritmo, deve ter dados de entrada, uma sequência de instruções, e fornecer dados de saída como resultado. Veja o exemplo abaixo, que se baseia na leitura do teclado com scanf:

  // Função meu_scanf:
  // Dados de entrada: parâmetro "char * mensagem" é o texto a ser mostrado antes de ler do teclado.
  // Dado de saída: meu_scanf devolve (retorna) um número inteiro que corresponde ao valor lido.

  int meu_scanf(char * mensagem) {
    int x, n;

    do {
      printf(mensagem);
      n = scanf("%d", &x);

      // se n == 0, então scanf não conseguiu ler o valor, porque ele não é do tipo inteiro.
      // Então o valor digitado deve ser descartado. Para descartar tudo o que foi digitado, deve-se
      // chamar de novo o scanf, mas com o formato "%*[^\n]". Isto joga fora toda a linha digitada.
      // Uma explicação sobre formatos do scanf está na sua página de manual (execute "man scanf").
      if (n == 0) {
        n = scanf("%*[^\n]");
      }
    } while (n == 0);

    // aqui meu_scanf retorna o valor lido, que está contido na variável "x"
    return x;
  }

  int main() {
    int opcao;

    do {
      // aqui mostra as opções do menu

      // chama a função "meu_scanf" para ler um número inteiro do teclado.
      opcao = meu_scanf("Digite uma opção: ");

      printf("Opção escolhida=%d\n", opcao);

      // executa a ação correspondente à opção escolhida

    } while (opcao != 7); // neste exemplo, a opção 7 faz a saída do programa
  }

No final destas transparências há um resumo sobre funções. Mais informações estão no curso de linguagem C da UFMG.

28/10 (turma 2070115)

A representação do que é um evento dentro do programa da agenda implica descrever todas as características de um evento, conforme descritas na seção Descrição. Há mais de uma forma de fazer isto dentro do programa, sendo a mais elementar (mas não a melhor !) simplesmente criar uma variável para cada dado que compõe um evento. Se for adotada essa abordagem, o resultado seria parecido com o exemplo abaixo:

int main() {
  char titulo[64];
  char descricao[1024];
  char local[64];
  int inicio; // o timestamp da data e horario de inicio
  int termino; // o timestamp da data e horario de término
  int criacao; // o timestamp da data e horario de criacao
  char categoria[32];
  char status[32];
  char recorrencia[32];

  // corpo do programa da agenda, com as instrucoes
}

Parece simples, mas fazer como mostrado acima traz várias dificuldades. A maior delas é não ser possível tratar um evento como um dado único dentro do programa, já que ele passa a ser definido por um conjunto de variáveis. Imagine a dificuldade de criar um vetor de eventos, ou de criar funções que recebam eventos como parâmetro. Isto leva a considerar que a possibilidade de um evento ser descrito por uma única variável facilitaria bastante a escrita do programa. No entanto, como conciliar isto com o fato de que um evento possui múltiplas características ? Uma solução pode ser obtida usando-se o que se chama de struct na linguagem C. Para entender o que é um struct, veja o exemplo abaixo sobre um cadastro de pessoas::

// Define uma struct Pessoa, que contem os dados
// (atributos) de uma pessoa, do ponto de vista deste programa.
// Repare que "struct Pessoa" passa a ser um novo tipo de dados !
struct Pessoa {
  char nome[32];
  char sobrenome[32];
  char endereco[128];
  int numero;
  char bairro[32];
  char cidade[64];
};

int main() {
  // Declara uma variável do tipo "struct Pessoa"
  struct Pessoa umaPessoa;

  printf("Nome: ");
  scanf("%s", umaPessoa.nome);
  printf("Sobrenome: ");
  scanf("%s", umaPessoa.sobrenome);
  printf("Endereco: ");
  scanf("%s", umaPessoa.endereco);
  printf("Numero da residencia: ");
  scanf("%d", &umaPessoa.numero);
  printf("Bairro: ");
  scanf("%s", umaPessoa.bairro);
  printf("Cidade: ");
  scanf("%s", umaPessoa.cidade);

  // mais instruções do programa que cria o cadastro

  return 0;
}

Nesse exemplo, uma pessoa é descrita por seu nome e sobrenome, endereco, número de residência, bairro e cidade. Para representar uma pessoa, cria-se um novo tipo de dados chamado struct Pessoa, que encapsula todas essas informações. Assim, as variáveis do tipo struct Pessoa serão capazes de conter todos esses dados, porém guardados de forma a ficarem facilmente vinculados. Note que na declaração de struct Pessoa estão declarações parecidas com variáveis, uma para cada característica de pessoa. Essas declarações contidas na struct se chamam campos.

// Define uma struct Pessoa, que contem os dados
// (atributos) de uma pessoa, do ponto de vista deste programa.
// Repare que "struct Pessoa" passa a ser um novo tipo de dados !
struct Pessoa {
  char nome[32]; // campo nome
  char sobrenome[32]; // campo sobrenome
  char endereco[128]; // campo endereço
  int numero; // campo número
  char bairro[32]; // campo bairro
  char cidade[64]; // campo cidade
};

Note que o acesso aos campos de uma variável do tipo struct se faz com a sintaxe identificador.campo (lembre que identificador é o nome de uma variável, neste caso o da variável do tipo struct). Veja este exemplo para mostrar os valores dos campos de uma variável do tipo struct Pessoa:

  printf("Nome: %s\n", pessoa.nome);
  printf("Sobrenome: %s\n", pessoa.sobrenome);
  printf("Endereco: %s\n", pessoa.endereco);
  printf("Numero da residencia: %d\n", pessoa.numero);
  printf("Bairro: %s\n", pessoa.bairro);
  printf("Cidade: %s\n", pessoa.cidade);

Finalmente, uma variável do tipo struct pode ser passada como parâmetro de uma função. Isto demonstra inclusive que a variável do tipo struct é tratada como uma coisa única, apesar de conter muitos valores de diferentes tipos internamente:

struct Pessoa {
  char nome[32]; // campo nome
  char sobrenome[32]; // campo sobrenome
  char endereco[128]; // campo endereço
  int numero; // campo número
  char bairro[32]; // campo bairro
  char cidade[64]; // campo cidade
};

// Uma funcao para mostrar os dados de uma variavel do tipo "struct Pessoa".
// Essa função recebe como parâmetro um valor do tipo "struct Pessoa", e esse
// valor será copiado para dentro da variável "pessoa". Essa variável existe somente
// dentro desta função (quer dizer, é uma variável local).
// A função não devolve nenhum valor, por isto seu tipo de retorno é "void".

void mostra_pessoa(struct Pessoa pessoa) {
  printf("Nome: %s\n", pessoa.nome);
  printf("Sobrenome: %s\n", pessoa.sobrenome);
  printf("Endereco: %s\n", pessoa.endereco);
  printf("Numero da residencia: %d\n", pessoa.numero);
  printf("Bairro: %s\n", pessoa.bairro);
  printf("Cidade: %s\n", pessoa.cidade);
}

int main() {
  struct Pessoa umaPessoa;

  printf("Nome: ");
  scanf("%s", umaPessoa.nome);
  printf("Sobrenome: ");
  scanf("%s", umaPessoa.sobrenome);
  printf("Endereco: ");
  scanf("%s", umaPessoa.endereco);
  printf("Numero da residencia: ");
  scanf("%d", &umaPessoa.numero);
  printf("Bairro: ");
  scanf("%s", umaPessoa.bairro);
  printf("Cidade: ");
  scanf("%s", umaPessoa.cidade);

  printf("\n\nVoce digitou o seguinte:\n\n");
  mostra_pessoa(umaPessoa);

  return 0;
}

Atenção: declarações de novos tipos de dados com struct devem ser globais (fora da função main', e também de qualquer outra função).

Mais um exemplo do uso de struct e funções: data e horário

Neste novo exemplo se deseja fazer um programa que leia e mostre datas. Uma data é definida por: dia, mês, ano, hora, minuto e segundo. Assim, foi definido um novo tipo de dados para representar datas no programa:

struct DataHorario {
  int dia, mes, ano;
  int hora, minuto, segundo;
};

O novo programa deve ler uma data do teclado, e mostrar na tela a data lida. Assim dois algoritmos são necessários: um para ler a data, e outro para mostrá-la. Esses algoritmos foram implementados em funções, como mostrado abaixo:

// Função mostra_data: mostra uma data no formato dia/mes/ano hora:minuto:segundo
// dado de entrada: um valor do tipo "struct DataHorario", a ser passado no parâmetro "umaData"
// valor de retorno: não tem (por isto é do tipo "void")

void mostra_data(struct DataHorario umaData) {
  printf("%d/%d/%d %d:%d:%d", umaData.dia, umaData.mes, umaData.ano,
                              umaData.hora, umaData.minuto, umaData.segundo);
}

// Função le_data: lê uma data, que deve ser digitada no formato dia/mes/ano hora:minuto:segundo
// dado de entrada: não tem
// valor de retorno: um valor do tipo "struct DataHorario", que corresponde a data lida

struct DataHorario le_data() {
  struct DataHorario umaData;

  scanf("%d/%d/%d %d:%d:%d", &umaData.dia, &umaData.mes, &umaData.ano,
                              &umaData.hora, &umaData.minuto, &umaData.segundo);

  return umaData;
}

Agora pode-se reunir a declaração de struct DataHorario e das funções le_data e mostra_data para escrever o programa completo que lê data e a mostra na tela:

#include <stdio.h>

struct DataHorario {
  int dia, mes, ano;
  int hora, minuto, segundo;
};

// Função mostra_data: mostra uma data no formato dia/mes/ano hora:minuto:segundo
// dado de entrada: um valor do tipo "struct DataHorario", a ser passado no parâmetro "umaData"
// valor de retorno: não tem (por isto é do tipo "void")

void mostra_data(struct DataHorario umaData) {
  printf("%d/%d/%d %d:%d:%d", umaData.dia, umaData.mes, umaData.ano,
                              umaData.hora, umaData.minuto, umaData.segundo);
}

// Função le_data: lê uma data, que deve ser digitada no formato dia/mes/ano hora:minuto:segundo
// dado de entrada: não tem
// valor de retorno: um valor do tipo "struct DataHorario", que corresponde a data lida

struct DataHorario le_data() {
  struct DataHorario umaData;

  scanf("%d/%d/%d %d:%d:%d", &umaData.dia, &umaData.mes, &umaData.ano,
                              &umaData.hora, &umaData.minuto, &umaData.segundo);

  return umaData;
}

int main() {
  struct Data aData;

  printf("Informe a data no formato dia/mes/ano hora:minuto:segundo: ");
  data = le_data();

  printf("Voce informou: ");
  mostra_data(data);
  puts("");
}