Novos overloads na classe ISOWeek para o tipo DateOnly no C#
O ISOWeek com DateOnly chegou no .NET 10 por meio de novos overloads que eliminam a necessidade de usar DateTime para calcular semanas ISO 8601. A classe ISOWeek já existia para permitir que desenvolvedores manipulassem as semanas do ano de maneira confiável, mas até então funcionava exclusivamente com DateTime.
Com a chegada do tipo DateOnly no .NET 6, fazia sentido que a classe ISOWeek também suportasse esse tipo. Neste artigo, vamos ver como lidávamos com semanas ISO antes dessa novidade, por que essa melhoria é bem-vinda e como utilizá-la no código.
O que é a classe ISOWeek
A ISOWeek é uma classe estática do namespace System.Globalization. Segundo a documentação oficial da classe ISOWeek, ela oferece membros para trabalhar com a data em semanas definida pelo padrão ISO 8601, publicado pela International Organization for Standardization.
A classe está disponível desde o .NET Core 3.0, bem antes do DateOnly existir. Antes disso, quem precisava do número da semana ISO tinha que escrever o próprio cálculo ou confiar em métodos que não seguem o padrão à risca, como veremos adiante.
Os principais métodos são GetWeekOfYear, que devolve o número da semana, e GetYear, que devolve o ano ISO. Além deles, existem ToDateTime, GetWeeksInYear, GetYearStart e GetYearEnd.
Como funciona a semana ISO 8601
O padrão ISO 8601 define um calendário baseado em semanas, muito usado em relatórios, planejamento de produção e sistemas financeiros. Nesse calendário, toda semana começa na segunda-feira e termina no domingo.
A primeira semana do ano é aquela que contém a primeira quinta-feira de janeiro. Em outras palavras, a semana 1 precisa ter pelo menos quatro dias dentro do novo ano.
Por consequência, um ano ISO tem 52 ou 53 semanas. Por exemplo, os anos ISO de 2020 e 2026 têm 53 semanas, enquanto 2024 e 2025 têm 52.
Por que o ano ISO pode ser diferente do ano civil
Como a semana 1 depende da primeira quinta-feira, os primeiros e os últimos dias de janeiro e dezembro podem pertencer a outro ano ISO. Esse é o ponto que mais causa bugs em sistemas que trabalham com semanas.
A própria documentação da Microsoft traz um exemplo: o dia 1º de janeiro de 2005 cai na semana 53 do ano ISO de 2004. Da mesma forma, 30 e 31 de dezembro de 2024 pertencem à semana 1 do ano ISO de 2025.
Por isso, você sempre deve combinar o número da semana com o ano ISO, e nunca com o ano civil da data. Caso contrário, um relatório pode juntar dias de semanas diferentes no mesmo grupo.
Semana ISO versus semana da cultura local
Vale lembrar que a semana ISO nem sempre coincide com a forma como o usuário enxerga a semana. Por exemplo, em muitos calendários brasileiros a semana começa no domingo, e não na segunda-feira.
No .NET, essa preferência fica em CultureInfo.DateTimeFormat.FirstDayOfWeek, que varia conforme a cultura. Já o ISOWeek ignora a cultura e aplica sempre a mesma regra.
Por isso, defina com o time de negócio qual semana o sistema usa. Se a resposta for "semana ISO", o ISOWeek com DateOnly é a ferramenta certa, e o resultado será o mesmo em qualquer servidor.
Por que o Calendar.GetWeekOfYear não basta
O .NET sempre ofereceu o método Calendar.GetWeekOfYear, que recebe uma regra (CalendarWeekRule) e o primeiro dia da semana. À primeira vista, usar CalendarWeekRule.FirstFourDayWeek com DayOfWeek.Monday parece resolver o problema.
No entanto, a documentação avisa que esse método não corresponde exatamente ao ISO 8601. Na prática, ele pode divergir em datas próximas da virada do ano e também não devolve o ano ISO.
Segundo a própria documentação, a partir do .NET Core 3.0 os métodos ISOWeek.GetYear e ISOWeek.GetWeekOfYear resolvem essa diferença. Portanto, quando a regra de negócio fala em semana ISO, a classe ISOWeek é o caminho correto.
O tipo DateOnly em resumo
O DateOnly chegou no .NET 6 para representar apenas uma data, sem hora e sem fuso horário. Ele resolve um problema antigo: guardar datas de nascimento, vencimentos ou feriados em um DateTime, que sempre carrega um horário junto.
Com o DateTime, um vencimento em 08/03/2024 vira 08/03/2024 às 00:00:00. Assim, conversões de fuso horário ou serializações podem mudar o dia sem que ninguém perceba.
Já o DateOnly deixa a intenção explícita no tipo, e é por isso que o ISOWeek com DateOnly faz tanto sentido. Se quiser entender melhor as diferenças entre os tipos de data do .NET, confira o nosso guia sobre DateTime no C# e .NET.
Como fazíamos antes do ISOWeek com DateOnly
Antes desses novos overloads, era necessário usar a classe ISOWeek com DateTime, mesmo que a informação de hora não fosse relevante. Para quem trabalhava só com datas, isso significava carregar dados desnecessários, o que aumenta a complexidade do código e o risco de erros.
Veja um exemplo de uso com DateTime:
DateTime date = new DateTime(2024, 3, 8);
int week = ISOWeek.GetWeekOfYear(date);
int year = ISOWeek.GetYear(date);
Console.WriteLine($"Date: {date:yyyy-MM-dd}, Week: {week}, ISO year: {year}");
// Date: 2024-03-08, Week: 10, ISO year: 2024
Neste caso, mesmo sem precisar da hora (HH:mm:ss), ainda era preciso trabalhar com um DateTime.
Quando o domínio já usava DateOnly, a situação ficava pior. Afinal, cada chamada exigia uma conversão de ida e, às vezes, uma de volta.
DateOnly dueDate = new DateOnly(2024, 3, 8);
// conversão obrigatória antes do .NET 10
DateTime dueDateTime = dueDate.ToDateTime(TimeOnly.MinValue);
int week = ISOWeek.GetWeekOfYear(dueDateTime);
// e outra conversão para voltar ao DateOnly
DateOnly monday = DateOnly.FromDateTime(ISOWeek.ToDateTime(2024, week, DayOfWeek.Monday));
O código funciona, mas o ruído entre ISOWeek e DateOnly é evidente. Além disso, cada conversão é mais um ponto onde alguém pode passar um horário ou um DateTimeKind inesperado.
O que mudou no ISOWeek com DateOnly
A partir do .NET 10, a classe ISOWeek recebeu novos métodos sobrecarregados para o tipo DateOnly. Dessa forma, você trabalha com semanas ISO sem lidar com informações de hora.
A página de novidades das bibliotecas no .NET 10 explica a motivação: a equipe criou a classe antes do DateOnly existir, e agora faz sentido suportá-lo também. Os novos métodos são estes:
public static class ISOWeek
{
// novos overloads com DateOnly
public static int GetWeekOfYear(DateOnly date);
public static int GetYear(DateOnly date);
public static DateOnly ToDateOnly(int year, int week, DayOfWeek dayOfWeek);
}
O GetWeekOfYear(DateOnly) devolve um número entre 1 e 53. Já o GetYear(DateOnly) devolve o ano ISO, entre 1 e 9999.
Por fim, o ToDateOnly faz o caminho inverso. Ele recebe o ano ISO, a semana e o dia da semana, e devolve a data gregoriana equivalente.
Por que o ISOWeek com DateOnly é bem-vindo
Usar o ISOWeek com DateOnly traz várias vantagens:
- Código mais limpo e sem informações desnecessárias: agora trabalhamos com datas sem precisar de um
DateTime, que carrega uma hora irrelevante nesse contexto. - Menos risco de erros: o
DateTimepode gerar confusões com fusos horários ou horários implícitos, e oDateOnlyelimina essa preocupação. - Maior compatibilidade: aplicações que já usam
DateOnlyganham suporte natural às semanas ISO, sem conversões entre tipos.
Além disso, os overloads do ISOWeek para DateOnly mantêm a API consistente. Quem já conhece os métodos com DateTime não precisa aprender nada novo, porque os nomes e o comportamento são os mesmos.
Como usar o ISOWeek com DateOnly na prática
Com o ISOWeek com DateOnly, o código fica direto e intuitivo. A seguir, veja os cenários mais comuns.
Calcular a semana e o ano ISO
O primeiro uso é descobrir em qual semana ISO uma data cai. Para isso, basta passar o DateOnly diretamente ao ISOWeek.
DateOnly date = new DateOnly(2024, 3, 8);
int week = ISOWeek.GetWeekOfYear(date);
int year = ISOWeek.GetYear(date);
Console.WriteLine($"Date: {date:yyyy-MM-dd}, Week: {week}, ISO year: {year}");
// Date: 2024-03-08, Week: 10, ISO year: 2024
DateOnly fromWeek = ISOWeek.ToDateOnly(2024, week, DayOfWeek.Monday);
Console.WriteLine($"First day of week {week}: {fromWeek:yyyy-MM-dd}");
// First day of week 10: 2024-03-04
Repare que o ISOWeek.ToDateOnly já devolve um DateOnly. Ou seja, não existe mais nenhuma conversão no meio do caminho.
Montar o intervalo de uma semana
Outro cenário frequente é exibir o período de uma semana, como em "semana 10: de 04/03 a 10/03". Como a semana ISO sempre começa na segunda-feira, basta pegar esse dia e somar seis dias.
static (DateOnly Start, DateOnly End) GetIsoWeekRange(int isoYear, int week)
{
// a semana ISO sempre começa na segunda-feira
DateOnly start = ISOWeek.ToDateOnly(isoYear, week, DayOfWeek.Monday);
return (start, start.AddDays(6));
}
var (start, end) = GetIsoWeekRange(2024, 10);
Console.WriteLine($"Week 10: {start:yyyy-MM-dd} to {end:yyyy-MM-dd}");
// Week 10: 2024-03-04 to 2024-03-10
Esse tipo de função é útil, por exemplo, para filtrar pedidos ou lançamentos de uma semana em uma consulta.
Agrupar registros por semana ISO
Relatórios semanais são o caso clássico do ISOWeek com DateOnly. Nele, a chave do agrupamento precisa combinar o ano ISO e a semana, e não o ano civil.
public record Sale(DateOnly Date, decimal Amount);
var sales = new List<Sale>
{
new(new DateOnly(2024, 12, 30), 150m),
new(new DateOnly(2024, 12, 31), 90m),
new(new DateOnly(2025, 1, 6), 200m)
};
var weeklyTotals = sales
.GroupBy(sale => (Year: ISOWeek.GetYear(sale.Date), Week: ISOWeek.GetWeekOfYear(sale.Date)))
.Select(group => new { group.Key.Year, group.Key.Week, Total = group.Sum(sale => sale.Amount) });
foreach (var item in weeklyTotals)
{
Console.WriteLine($"{item.Year}-W{item.Week:D2}: {item.Total}");
}
// 2025-W01: 240
// 2025-W02: 200
Repare que as vendas de 30 e 31 de dezembro de 2024 caem na semana 1 de 2025. Se o código usasse sale.Date.Year, essas vendas iriam para um grupo "2024-W01", o que estaria errado.
Como migrar para o ISOWeek com DateOnly
Se o seu projeto já roda no .NET 10, a migração para o ISOWeek com DateOnly é mecânica. Em geral, basta procurar as conversões entre DateOnly e DateTime que existem só para chamar o ISOWeek.
O roteiro costuma seguir três passos:
- Localizar chamadas como
ToDateTime(TimeOnly.MinValue)usadas apenas antes deISOWeek.GetWeekOfYearouISOWeek.GetYear. - Passar o
DateOnlydireto para o método e remover a conversão. - Trocar
DateOnly.FromDateTime(ISOWeek.ToDateTime(...))porISOWeek.ToDateOnly(...).
Veja o mesmo trecho do exemplo anterior depois da migração:
DateOnly dueDate = new DateOnly(2024, 3, 8);
// sem conversões no .NET 10
int week = ISOWeek.GetWeekOfYear(dueDate);
DateOnly monday = ISOWeek.ToDateOnly(2024, week, DayOfWeek.Monday);
Como o comportamento dos overloads é o mesmo, os resultados não mudam. Ainda assim, vale manter os testes que cobrem as datas da virada do ano, porque são eles que garantem a regra de negócio.
Bibliotecas com vários targets
Se você mantém uma biblioteca que também compila para versões anteriores ao .NET 10, os novos overloads não existem nesses targets. Nesse caso, uma opção é isolar a chamada em um método auxiliar com compilação condicional.
public static class IsoWeekHelper
{
public static int GetWeekOfYear(DateOnly date)
{
#if NET10_0_OR_GREATER
return ISOWeek.GetWeekOfYear(date);
#else
// fallback para targets anteriores ao .NET 10
return ISOWeek.GetWeekOfYear(date.ToDateTime(TimeOnly.MinValue));
#endif
}
}
Dessa forma, o restante do código usa sempre DateOnly, e a conversão fica concentrada em um único lugar.
ISOWeek com DateOnly em uma API
Em APIs, o ISOWeek com DateOnly ajuda a expor relatórios semanais com contratos claros. Por exemplo, uma rota pode receber o ano ISO e a semana e devolver o intervalo de datas correspondente.
var app = WebApplication.Create(args);
app.MapGet("/reports/weeks/{isoYear:int}/{week:int}", (int isoYear, int week) =>
{
// rejeita semanas que não existem no ano ISO informado
if (week < 1 || week > ISOWeek.GetWeeksInYear(isoYear))
return Results.BadRequest("Invalid ISO week.");
DateOnly start = ISOWeek.ToDateOnly(isoYear, week, DayOfWeek.Monday);
return Results.Ok(new { isoYear, week, start, end = start.AddDays(6) });
});
app.Run();
O System.Text.Json serializa DateOnly no formato yyyy-MM-dd desde o .NET 7. Assim, o cliente recebe apenas a data, sem horário nem fuso horário para interpretar.
Cuidados com o ISOWeek com DateOnly na virada do ano
Os novos overloads não mudam as regras do ISO 8601. Portanto, os mesmos cuidados de antes continuam valendo para o ISOWeek com DateOnly.
DateOnly newYear2005 = new DateOnly(2005, 1, 1);
Console.WriteLine($"{ISOWeek.GetYear(newYear2005)}-W{ISOWeek.GetWeekOfYear(newYear2005)}");
// 2004-W53
DateOnly lastFriday = ISOWeek.ToDateOnly(2020, 53, DayOfWeek.Friday);
Console.WriteLine($"{lastFriday:yyyy-MM-dd}");
// 2021-01-01
No primeiro caso, a data é de 2005, mas o ano ISO é 2004. No segundo, a sexta-feira da semana 53 de 2020 já cai em 2021.
Além disso, quando a semana vier do usuário, compare o valor com ISOWeek.GetWeeksInYear. Assim, você evita aceitar uma semana 53 em um ano ISO que só tem 52 semanas.
Se a sua regra depende de datas e horários reais, vale também tornar o código testável. Nesse caso, o nosso artigo sobre como testar data e hora no .NET mostra como isolar o relógio do sistema.
Métodos que continuam só com DateTime
Nem todos os membros do ISOWeek ganharam versão com DateOnly. Os métodos GetYearStart e GetYearEnd recebem o ano ISO e continuam devolvendo DateTime.
Enquanto isso, o GetWeeksInYear recebe um inteiro e devolve 52 ou 53, então não depende do tipo de data. Para os dois primeiros, a conversão ainda é simples:
// início e fim do ano ISO 2025 como DateOnly
DateOnly yearStart = DateOnly.FromDateTime(ISOWeek.GetYearStart(2025));
DateOnly yearEnd = DateOnly.FromDateTime(ISOWeek.GetYearEnd(2025));
int weeks = ISOWeek.GetWeeksInYear(2025);
Console.WriteLine($"{yearStart:yyyy-MM-dd} to {yearEnd:yyyy-MM-dd}, {weeks} weeks");
// 2024-12-30 to 2025-12-28, 52 weeks
Na prática, essas chamadas aparecem com menos frequência do que GetWeekOfYear e GetYear. Por isso, a conversão pontual não chega a pesar no código.
Quando continuar usando DateTime
Os overloads antigos com DateTime continuam disponíveis, e a Microsoft não os marcou como obsoletos. Portanto, se a informação de origem realmente tem hora, como o instante de um evento, não existe motivo para trocar o tipo.
Nesse caso, porém, converta o instante para o fuso horário do negócio antes de calcular a semana. Afinal, um evento às 23h de domingo em São Paulo já é segunda-feira em UTC, e isso muda a semana ISO.
Conclusão
A chegada do ISOWeek com DateOnly no .NET 10 é uma adição bem-vinda ao C#, porque deixa o código mais limpo e intuitivo para quem trabalha com semanas ISO. Agora, é possível manipular essas informações sem a complexidade extra do DateTime.
Ao mesmo tempo, as regras do ISO 8601 continuam exigindo atenção, principalmente na virada do ano. Combine sempre a semana com o ano ISO e use ToDateOnly para voltar da semana para a data.
Se você já usa DateOnly nas suas aplicações, essa novidade certamente vai simplificar o seu código. Para conhecer outras mudanças pequenas e úteis da mesma versão, veja também as mudanças no TimeSpan.FromMilliseconds no .NET 10.
FAQ
Em qual versão do .NET o ISOWeek aceita DateOnly?
Os overloads com DateOnly chegaram no .NET 10. Em versões anteriores, é preciso converter para DateTime.
Qual a diferença entre GetYear e a propriedade Year do DateOnly?
O Year devolve o ano civil da data. Já o ISOWeek.GetYear devolve o ano ISO, que pode ser diferente nos primeiros e últimos dias do ano.
Uma semana ISO sempre começa na segunda-feira?
Sim. No ISO 8601, a semana vai de segunda-feira a domingo, e a semana 1 contém a primeira quinta-feira do ano.
Posso usar Calendar.GetWeekOfYear no lugar do ISOWeek?
Não recomendamos. A documentação informa que esse método não corresponde exatamente ao ISO 8601.
Quantas semanas tem um ano ISO?
Um ano ISO tem 52 ou 53 semanas, e o método ISOWeek.GetWeeksInYear informa o valor de cada ano.