【问题标题】:How to cut the first Sunday to Saturday of each month in a year?一年中每个月的第一个周日到周六怎么剪?
【发布时间】:2020-08-19 19:06:29
【问题描述】:

我们有绿色区域逻辑,其中作业只能在第一个星期日到星期六之间运行,即从每个月的第一个星期日开始的 7 天。我正在使用下面的 awk 命令来获取它,但是它在某个地方被破坏了。我只是在尝试前 3 个月,即 1 月到 3 月

seq 75  | awk ' BEGIN {ti=" 0 0 0"} 
function dtf(fmt,dy) { return strftime(fmt,mktime("2020 1 " dy ti)) } 
{ day=dtf("%A %F",$0);mm=dtf("%m",$0);if(day~/Sunday/ || a[mm]) a[mm]++ ; if(a[mm]<8) print day }  '

我的输出如下,不正确:

Wednesday 2020-01-01
Thursday 2020-01-02
Friday 2020-01-03
Saturday 2020-01-04
Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Saturday 2020-02-01
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07

预期输出:

Sunday 2020-01-05
Monday 2020-01-06
Tuesday 2020-01-07
Wednesday 2020-01-08
Thursday 2020-01-09
Friday 2020-01-10
Saturday 2020-01-11
Sunday 2020-02-02
Monday 2020-02-03
Tuesday 2020-02-04
Wednesday 2020-02-05
Thursday 2020-02-06
Friday 2020-02-07
Saturday 2020-02-08
Sunday 2020-03-01
Monday 2020-03-02
Tuesday 2020-03-03
Wednesday 2020-03-04
Thursday 2020-03-05
Friday 2020-03-06
Saturday 2020-03-07

如何调整 awk 命令以获得预期的输出? 也欢迎使用其他 bash 工具的任何其他解决方案。

【问题讨论】:

    标签: bash perl awk


    【解决方案1】:

    我建议使用以下 awk 替代方案:

    #! /usr/bin/env bash
    for month in {01..03}; do
      for day in {01..13}; do
        date -d "2020-$month-$day" '+%A %F'
      done |
      grep -A6 -m1 -F Sunday
    done
    

    脚本效率不高,但能胜任。对于每个月,我们只打印该月前 13 天的日期。我们知道绿色区域必须在该区域,因此我们不需要该月的剩余天数。
    日期格式为Weekday YYYY-MM-DD。我们使用grep 查找并打印第一个星期日,打印该星期日之后的 6 天 (-A6) 并退出,因为我们将搜索限制为一个匹配项 (-m1)。
    上述程序在第 1 至第 3 个月中的每个月份进行。

    【讨论】:

      【解决方案2】:

      这是让 GNU awk 为任何给定年份创建日期和日期名称列表的简单方法:

      $ cat tst.awk
      BEGIN {
          year = (year == "" ? 2020 : year)
          beg = mktime(year " 1 1 12 0 0")
          for (i=0; i<=400; i++) {
              dateday = strftime("%F %A", beg+24*60*60*i)
              split(dateday,d,/[ -]/)
              if ( d[1] != year ) {
                  break
              }
              print d[1], d[2], d[3], d[4]
          }
      }
      

      .

      $ awk -f tst.awk | head -20
      2020 01 01 Wednesday
      2020 01 02 Thursday
      2020 01 03 Friday
      2020 01 04 Saturday
      2020 01 05 Sunday
      2020 01 06 Monday
      2020 01 07 Tuesday
      2020 01 08 Wednesday
      2020 01 09 Thursday
      2020 01 10 Friday
      2020 01 11 Saturday
      2020 01 12 Sunday
      2020 01 13 Monday
      2020 01 14 Tuesday
      2020 01 15 Wednesday
      2020 01 16 Thursday
      2020 01 17 Friday
      2020 01 18 Saturday
      2020 01 19 Sunday
      2020 01 20 Monday
      

      我从中午开始,从 0 天循环到 400 天,并在年份变化时中断,这样我就不必尝试适应 DST 或闰年或闰秒来确定一年中的天数计算更准确。

      只需添加一些代码来测试当前月份与前一个月不同,当前日期名称为星期日,然后从那里打印 7 天,例如:

      $ cat tst.awk
      BEGIN {
          year = (year == "" ? 2020 : year)
          beg = mktime(year " 1 1 12 0 0")
          for (i=0; i<=400; i++) {
              dateday = strftime("%F %A", beg+24*60*60*i)
              split(dateday,d,/[ -]/)
              if ( d[1] != year ) {
                  break
              }
              dayName[d[2]+0][d[3]+0] = d[4]
          }
          for (monthNr=1; monthNr<=3; monthNr++) {
              for (dayNr=1; dayNr in dayName[monthNr]; dayNr++) {
                  if (dayName[monthNr][dayNr] == "Sunday") {
                      for (i=0; i<7; i++) {
                          printf "%s %04d-%02d-%02d\n", dayName[monthNr][dayNr+i], year, monthNr, dayNr+i
                      }
                      break
                  }
              }
          }
      }
      

      .

      $ awk -f tst.awk
      Sunday 2020-01-05
      Monday 2020-01-06
      Tuesday 2020-01-07
      Wednesday 2020-01-08
      Thursday 2020-01-09
      Friday 2020-01-10
      Saturday 2020-01-11
      Sunday 2020-02-02
      Monday 2020-02-03
      Tuesday 2020-02-04
      Wednesday 2020-02-05
      Thursday 2020-02-06
      Friday 2020-02-07
      Saturday 2020-02-08
      Sunday 2020-03-01
      Monday 2020-03-02
      Tuesday 2020-03-03
      Wednesday 2020-03-04
      Thursday 2020-03-05
      Friday 2020-03-06
      Saturday 2020-03-07
      

      有一些更有效的方法可以做到这一点,但上述方法清晰简单,一眨眼就可以运行。

      【讨论】:

        【解决方案3】:

        一个(相当罗嗦 - 我没有时间让它更短:-))Perl 解决方案:

        #!/usr/bin/perl
        
        use strict;
        use warnings;
        use feature 'say';
        
        use Time::Piece;
        use Time::Seconds;
        
        my $year = shift || localtime->year;
        
        first_week($year, $_) for 1 ..12;
        
        
        sub first_week {
          my ($yr, $mn) = @_;
        
          $mn = sprintf '%02d', $mn;
        
          # Use midday to avoid DST issues
          my $start = Time::Piece->strptime(
            "$year-$mn-01 12:00:00",
            '%Y-%m-%d %H:%M:%S'
          );
        
          $start += ONE_DAY while $start->day ne 'Sun';
        
          for (1 .. 7) {
            say $start->strftime('%A %Y-%m-%d');
            $start += ONE_DAY;
          }
        }
        

        【讨论】:

        • @stack0114106:这是因为 ONE_DAY 常量正好是 24 小时,并不是所有的日子都是 24 小时(因为 DST 更改)。从中午开始可以最大程度地减少该问题将您带到错误的一天的机会。
        • 感谢您考虑所有可能的情况并使其稳健。
        【解决方案4】:

        试试这个

        for i in $(seq 12); do  cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{ for (j=0;j<=6;j++){print "2020-"month"-"$1+j;}exit}'
        

        编辑:更新打印日代码

        for i in $(seq 2); do  cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{for (j=0;j<=6;j++){print strftime("%A %F", mktime("2020 " month " " $1+j " 0 0 0"))}exit}'; done;
        

        一月和二月的演示

        $for i in $(seq 2); do  cal ${i} 2020 | awk -v month=${i} 'NF==7 && !/^Su/{a[0]="Sunday";a[1]="Monday";a[2]="Tuesday";a[3]="Wednesday";a[4]="Thursday";a[5]="Friday";a[6]="Saturday";for (j=0;j<=6;j++){print a[j]" " "2020-"month"-"$1+j}exit}'; done;
        Sunday 2020-1-5
        Monday 2020-1-6
        Tuesday 2020-1-7
        Wednesday 2020-1-8
        Thursday 2020-1-9
        Friday 2020-1-10
        Saturday 2020-1-11
        Sunday 2020-2-2
        Monday 2020-2-3
        Tuesday 2020-2-4
        Wednesday 2020-2-5
        Thursday 2020-2-6
        Friday 2020-2-7
        Saturday 2020-2-8
        $
        

        【讨论】:

        • 这也有效.. 你能用 mktime() 函数包装它吗?
        • @Socowi 添加了打印 Day 的功能。此外,Demo 是一个拼写错误。请立即查看
        • @stack0114106 检查我的编辑。 mktime() 需要什么功能?
        • 这种方法的好处是您不需要 GNU date 或 GNU awk(用于 strftime() 和 mktime())。 @DigvijayS,您应该始终从 1 开始 awk 数组,而不是 0,因为这就是所有 awk 提供的字符串、数组和字段的编号方式。鉴于您可以使用split("Monday Tuesday ... Saturday",a) 而不是手动输入a[1]="Monday"; a[2]="Tuesday"; ... a[7]="Saturday"。如果日期名称的 2 个字母缩写是可以接受的,您可以使用 NR==2{split($0,a)}。
        • @stack0114106 谢谢。更新了代码。 @Ed 谢谢。今后将从 0 开始初始化数组。
        【解决方案5】:

        对于 Perl,使用 DateTime

        use warnings;
        use strict;
        use feature 'say';
        
        use DateTime;
        
        my $dt = DateTime->new(year => 2020, month => 1, day => 1); 
        
        my $first_sunday = 7 - $dt->day_of_week + 1;  # day of month for first Sun
        
        while (1) { 
            my $day = $dt->day; 
            if ($day >= $first_sunday and $day < $first_sunday + 7) { 
                say $dt->ymd, " (", $dt->day_abbr, ")";
            }
        } 
        continue { 
            $dt->add(days => 1); 
            if ($dt->day == 1) {  # new month
                last if $dt->month > 3;
                $first_sunday = 7 - $dt->day_of_week + 1;
            }   
        }
        

        这保持一个状态(在一个月的第一天找出第一个星期日是哪一天),如果程序旨在生成并遍历感兴趣的范围内的所有日期,那么什么是非常合适的。

        另一方面,程序可能需要检查某一天;也许它每天都运行并且需要检查那一天。然后更简单的看一天是否在该月的第一个和第二个星期日之间

        my $dt = DateTime->today;
        
        while ( $dt->add(days => 1)->month <= 3) {
        
            if ($dt->day_of_week == 7) {           # it's a Sunday
                if ($dt->weekday_of_month == 1) {  # first Sunday in the month
                    say $dt->ymd, " (", $dt->day_abbr, ")";
                }
            } 
            else {
                my $sdt = $dt->clone;                # preserve $dt
                $sdt->subtract( $dt->day_of_week );  # drop to previous Sunday
                if ($sdt->weekday_of_month == 1) {   # was first Sunday in the month
                    say $dt->ymd, " (", $dt->day_abbr, ")";
                }
            }
        }
        

        代码周围的while 循环便于检查。

        对于星期日以外的日子,我们会退回到过去的星期日,以检查那是否是该月的第一个星期日。如果是这样,那么我们的一天就在所需的时间间隔内。如果这一天是星期天,我们只需要检查它是否是本月的第一天。

        如果重要的话,可以使代码更加高效和简洁

        if ( (my $dow = $dt->day_of_week) == 7) { 
            if ($dt->weekday_of_month == 1) {
                say $dt->ymd, " (", $dt->day_abbr, ")";
            }
        }   
        elsif ( $dt->clone->subtract(days => $dow)->weekday_of_month == 1 ) { 
            say $dt->ymd, " (", $dt->day_abbr, ")";
        }
        

        ...考虑到可读性。

        【讨论】:

          【解决方案6】:
          $ printf "%s\n" 2020-{01..03}-01                 \
            | xargs -I{} date -d "{}" "+{} %u"             \
            | join -j3 - <(seq 0 6)                        \
            | xargs -n3 sh -c 'date -d "$1 + 7 days - $2 days + $3 days" "+%A %F"' --
          

          这里有一些讨厌的东西,但我会试着解释一下。这个想法是计算该月第一天的星期几(假设u)。如果您知道这一点,您就可以直接知道第一个星期天是哪一天(7-u 天后)。因此,从那时起,您只需计算接下来的 6 天。

          1. 使用大括号扩展生成您感兴趣的月份
          2. 使用xargs计算星期几并输出为YYYY-MM-DD u
          3. 每天,我们想要创建一个包含 7 个字符串 YYYY-MM-DD u d 的列表,其中 d 从 0 到 6 运行。为此,我们使用了一个讨厌的 join hack。通过告诉join 加入不存在字段上的文件,我们创建了一个外部产品。
          4. 结合使用xargs 和sh 创建一个接受3 个参数并进行计算的命令。

          这种方法现在很容易扩展到其他月份和年份:

          $ printf "%s\n" 20{20..30}-{01..12}-01  | xargs ...
          

          上面看起来有点乱,你可能对循环版本更感兴趣:

          for yyyymm in {2020..2030}-{01..03}; do 
            u=$(date -d "$yyyymm-01" "+%u");
            for ((dd=7-u;dd<14-u;++dd)); do
               date -d "$yyyymm-01 + $dd days" "+%A %F"
            done
          done
          

          以前的解决方案:

          这是 2020 年前 3 个月的数据:

          $ printf "%s\n" 2020-{01..03}-{01..13}           \
            | xargs -n1 -I{} date -d '{}' '+%A %F'         \
            | awk -F"[- ]" '/Sun/{a[$3]++} a[$3]==1'
          

          这是从 2020 年到 2030 年的第一年

          $ printf "%s\n" 20{20..30}-{01..12}-{01..13}     \
            | xargs -n1 -I{} date -d '{}' '+%A %F'         \
            | awk -F"[- ]" '/Sun/{a[$2,$3]++} a[$2,$3]==1'
          

          这分三步理解:

          1. 使用大括号扩展来创建您感兴趣的月份和年份的前 13 天的列表。这很有效,因为 bash 开始从左到右扩展。这意味着当天是快速运行的指数。我们要求前 13 天,因为我们知道第一个星期日必须在前 7 天内。

          2. 使用 xargs 和 date 将日期转换为预期格式

          3. 使用awk 进行过滤。

          【讨论】:

          【解决方案7】:

          通过再添加一个条件,我可以使它起作用。 a[mm]&lt;8 &amp;&amp; a[mm]&gt;0

          seq 75  | awk ' 
           BEGIN { ti=" 0 0 0" } 
           function dtf(fmt,dy) { 
            return strftime(fmt,mktime("2020 1 " dy ti)) 
           } 
          {  day=dtf("%A %F",$0);
             mm=dtf("%m",$0);
             if(day~/Sunday/ || a[mm]) a[mm]++ ; 
             if(a[mm]<8 && a[mm]>0 )  print day 
           }'
          

          输出:

          Sunday 2020-01-05
          Monday 2020-01-06
          Tuesday 2020-01-07
          Wednesday 2020-01-08
          Thursday 2020-01-09
          Friday 2020-01-10
          Saturday 2020-01-11
          Sunday 2020-02-02
          Monday 2020-02-03
          Tuesday 2020-02-04
          Wednesday 2020-02-05
          Thursday 2020-02-06
          Friday 2020-02-07
          Saturday 2020-02-08
          Sunday 2020-03-01
          Monday 2020-03-02
          Tuesday 2020-03-03
          Wednesday 2020-03-04
          Thursday 2020-03-05
          Friday 2020-03-06
          Saturday 2020-03-07
          

          作为附加说明,虽然我为该月硬编码了 1,但当 day 参数 >31 时,mktime() 只会移动到下个月。因此,在某种程度上,您可以将儒略日传递给 mktime,并将月份设置为 1。

          echo -e "1\n31\n32\n60\n61\n366" | awk ' 
           BEGIN { ti=" 0 0 0" } 
           function dtf(fmt,dy) { 
            return strftime(fmt,mktime("2020 1 " dy ti)) 
           } 
          {  
             day=dtf("%A %F",$0);
             j=dtf("%j",$0); 
             print j,day 
           }'
          

          输出:

          001 Wednesday 2020-01-01
          031 Friday 2020-01-31
          032 Saturday 2020-02-01
          060 Saturday 2020-02-29
          061 Sunday 2020-03-01
          366 Thursday 2020-12-31
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-08-21
            • 2019-05-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-01-20
            相关资源
            最近更新 更多