Converti un intervallo di date in una descrizione di intervallo


11

Un requisito in un recente progetto era di segnalare quando una risorsa sarebbe stata completamente consumata. Oltre alla data del calendario di esaurimento, mi è stato chiesto di mostrare il tempo rimanente in formato inglese, qualcosa come "1 anno, 3 mesi alla fine".

La DATEDIFFfunzione integrata

Restituisce il conteggio ... dei limiti di datepart specificati attraversati tra la data di inizio e la data di fine specificate.

Se usato così com'è, ciò potrebbe produrre risultati fuorvianti o confusi. Ad esempio, l'utilizzo di un intervallo di ANNO mostrerebbe 1999-12-31 (AAAA-MM-GG) e 2000-01-01 distanti un anno, mentre il senso comune direbbe che queste date sono separate da un solo giorno. Al contrario, utilizzando un intervallo di DAY 1999-12-31 e 2010-12-31 sono separati da 4.018 giorni, mentre la maggior parte delle persone vedrebbe "11 anni" come una descrizione migliore.

A partire dal numero di giorni e calcolando mesi e anni da lì sarebbe incline a saltare l'anno e gli errori di dimensione del mese.

Mi chiedo come possa essere implementato nei vari dialetti SQL? L'output di esempio include:

create table TestData(
    FromDate date not null,
    ToDate date not null,
    ExpectedResult varchar(100) not null); -- exact formatting is unimportant

insert TestData (FromDate, ToDate, ExpectedResult)
values ('1999-12-31', '1999-12-31', '0 days'),
       ('1999-12-31', '2000-01-01', '1 day'),
       ('2000-01-01', '2000-02-01', '1 month'),
       ('2000-02-01', '2000-03-01', '1 month'),              -- month length not important
       ('2000-01-28', '2000-02-29', '1 month, 1 day'),       -- leap years to be accounted for
       ('2000-01-01', '2000-12-31', '11 months, 30 days'),
       ('2000-02-28', '2000-03-01', '2 days'),
       ('2001-02-28', '2001-03-01', '1 day'),                -- not a leap year
       ('2000-01-01', '2001-01-01', '1 year'),
       ('2000-01-01', '2011-01-01', '11 years'),
       ('9999-12-30', '9999-12-31', '1 day'),                -- catch overflow in date calculations
       ('1900-01-01', '9999-12-31', '8099 years 11 months 30 days');  -- min(date) to max(date)

Mi capita di usare SQL Server 2008R2 ma sono interessato a sapere come gli altri dialetti lo gestiranno.

Risposte:


9

La seguente soluzione è per SQL Server. L'approccio è simile a quello di Serg in quanto la query utilizza solo le funzioni DATEADD e DATEDIFF. Tuttavia, non tiene conto degli intervalli negativi ( FromDate > ToDate ) e deriva anni e mesi dalla differenza totale del mese:

WITH
  MonthDiff AS
  (
    SELECT
      t.FromDate,
      t.ToDate,
      t.ExpectedResult,
      Months = x.Months - CASE WHEN DAY(t.FromDate) > DAY(t.ToDate) THEN 1 ELSE 0 END
    FROM
      dbo.TestData AS t
      CROSS APPLY (SELECT DATEDIFF(MONTH, t.FromDate, t.ToDate)) AS x (Months)
  )
SELECT
  t.FromDate,
  t.ToDate,
  t.ExpectedResult,
  Result = ISNULL(NULLIF(ISNULL(x.Years  + CASE x.Years  WHEN '1' THEN ' year '  ELSE ' years '  END, '')
                       + ISNULL(x.Months + CASE x.Months WHEN '1' THEN ' month ' ELSE ' months ' END, '')
                       + ISNULL(x.Days   + CASE x.Days   WHEN '1' THEN ' day '   ELSE ' days '   END, ''), ''), '0 days')
FROM
  MonthDiff AS t
  CROSS APPLY
  (
    SELECT
      CAST(NULLIF(t.Months / 12, 0) AS varchar(10)),
      CAST(NULLIF(t.Months % 12, 0) AS varchar(10)),
      CAST(NULLIF(DATEDIFF(DAY, DATEADD(MONTH, t.Months, t.FromDate), t.ToDate), 0) AS varchar(10))
  ) AS x (Years, Months, Days)
;

Produzione:

FromDate    ToDate      ExpectedResult                 Result
----------  ----------  -----------------------------  -----------------------------
1999-12-31  1999-12-31  0 days                         0 days
1999-12-31  2000-01-01  1 day                          1 day 
2000-01-01  2000-02-01  1 month                        1 month 
2000-02-01  2000-03-01  1 month                        1 month 
2000-01-28  2000-02-29  1 month, 1 day                 1 month 1 day 
2000-01-01  2000-12-31  11 months, 30 days             11 months 30 days 
2000-02-28  2000-03-01  2 days                         2 days 
2001-02-28  2001-03-01  1 day                          1 day 
2000-01-01  2001-01-01  1 year                         1 year 
2000-01-01  2011-01-01  11 years                       11 years 
9999-12-30  9999-12-31  1 day                          1 day 
1900-01-01  9999-12-31  8099 years 11 months 30 days   8099 years 11 months 30 days 

10

Questa risposta mostra un'implementazione che utilizza una funzione CLR di SQL Server (2005+).

-- Enable CLR (if necessary)
EXECUTE sys.sp_configure 
    @configname = 'clr enabled',
    @configvalue = 1;

RECONFIGURE;

Assemblaggio e funzione

CREATE ASSEMBLY DBA
AUTHORIZATION dbo
FROM 0x4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C010300B11134570000000000000000E00002210B010B00000C000000060000000000000E2A0000002000000040000000000010002000000002000004000000000000000400000000000000008000000002000000000000030040850000100000100000000010000010000000000000100000000000000000000000B42900005700000000400000A802000000000000000000000000000000000000006000000C0000007C2800001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000140A000000200000000C000000020000000000000000000000000000200000602E72737263000000A80200000040000000040000000E0000000000000000000000000000400000402E72656C6F6300000C0000000060000000020000001200000000000000000000000000004000004200000000000000000000000000000000F0290000000000004800000002000500EC210000900600000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000133003008601000001000011020A0F01280600000A0F00280600000A590B160C160D072C5C0F00280700000A0F01280700000A30200F00280700000A0F01280700000A33140F00280800000A0F01280800000A31040717590B120007280900000A0A2B1D120017280A00000A03280B00000A2C5B0817580C120017280A00000A0A0603280C00000A2C451200280600000A7E0D00000A13051205280600000A33C31200280700000A7E0D00000A13061206280700000A33AC2B150917580D120023000000000000F03F280E00000A0A0603280C00000A2DE21F64730F00000A13040716313D1104076F1000000A26110407172E0772010000702B05720F0000706F1100000A2611040816300B091630077E1200000A2B05721B0000706F1100000A26081631391104086F1000000A26110408172E0772210000702B0572310000706F1100000A261104091630077E1200000A2B05721B0000706F1100000A2609163006072D24082D211104096F1000000A26110409172E07723F0000702B05724B0000706F1100000A2611046F1300000A2A1E02281400000A2A000042534A4201000100000000000C00000076322E302E35303732370000000005006C000000A8010000237E000014020000F001000023537472696E6773000000000404000058000000235553005C0400001000000023475549440000006C0400002402000023426C6F620000000000000002000001471502000900000000FA253300160000010000000A000000020000000200000003000000140000000500000001000000010000000200000000000A0001000000000006003D0036000600440036000A008E0073000600BB00A8001300CF0000000600FE00DE0006001E01DE000A00460173000600C501B9010600DA0136000000000001000000000001000100010010001800000005000100010050200000000096004D000A000100E22100000000861861001200040000000000000000000100A00000000200A500190061001200210061004800310061004E0039006100120041006100120011005B01B60111006401B60111006E01B60111007601BA0111007F01BA0111008901C00111009C01C0011100A801C8011100B101CC01490061004E004900D301D2014900D301D8015100E101DE010900E701E10109006100120020002B00530024000B0016002E001300F3012E001B00FC012E0023000502E5010480000000000000000000000000000000003C01000002000000000000000000000001002D000000000002000000000000000000000001006700000000000000003C4D6F64756C653E004461746162617365312E646C6C0055736572446566696E656446756E6374696F6E73006D73636F726C69620053797374656D004F626A656374004461746554696D6500496E74657276616C4465736372697074696F6E002E63746F720053797374656D2E44617461004D6963726F736F66742E53716C5365727665722E5365727665720053716C46616365744174747269627574650046726F6D00546F0053797374656D2E446961676E6F73746963730044656275676761626C6541747472696275746500446562756767696E674D6F6465730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300436F6D70696C6174696F6E52656C61786174696F6E734174747269627574650052756E74696D65436F6D7061746962696C697479417474726962757465004461746162617365310053716C46756E6374696F6E417474726962757465006765745F59656172006765745F4D6F6E7468006765745F446179004164645965617273004164644D6F6E746873006F705F4C6573735468616E4F72457175616C006F705F4C6573735468616E004D617856616C756500416464446179730053797374656D2E5465787400537472696E674275696C64657200417070656E6400537472696E6700456D70747900546F537472696E6700000D200079006500610072007300000B2000790065006100720000052C002000000F20006D006F006E00740068007300000D20006D006F006E0074006800000B200064006100790073000009200064006100790000000000AFDAAB526E833740886DDFF9139712E60008B77A5C561934E0890700020E1109110903200001310100030054020D497346697865644C656E6774680054020A49734E756C6C61626C65005408074D617853697A656400000005200101111504200101088161010005005455794D6963726F736F66742E53716C5365727665722E5365727665722E446174614163636573734B696E642C2053797374656D2E446174612C2056657273696F6E3D322E302E302E302C2043756C747572653D6E65757472616C2C205075626C69634B6579546F6B656E3D623737613563353631393334653038390A446174614163636573730000000054557F4D6963726F736F66742E53716C5365727665722E5365727665722E53797374656D446174614163636573734B696E642C2053797374656D2E446174612C2056657273696F6E3D322E302E302E302C2043756C747572653D6E65757472616C2C205075626C69634B6579546F6B656E3D623737613563353631393334653038391053797374656D446174614163636573730000000054020F497344657465726D696E69737469630154020949735072656369736501540E044E616D6513496E74657276616C4465736372697074696F6E0320000805200111090807000202110911090306110905200111090D05200112250805200112250E02060E0320000E0D070711090808081225110911090801000200000000000801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F77730100000000B111345700000000020000001C01000098280000980A000052534453F841C8A989DDDC4098D9FD78225EB30502000000633A5C55736572735C7061756C775C4F6E6544726976655C446F63756D656E74735C56697375616C2053747564696F20323031355C50726F6A656374735C4461746162617365315C4461746162617365315C6F626A5C52656C656173655C4461746162617365312E706462000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000DC2900000000000000000000FE290000002000000000000000000000000000000000000000000000F02900000000000000000000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF2500200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001001000000018000080000000000000000000000000000001000100000030000080000000000000000000000000000001000000000048000000584000004C02000000000000000000004C0234000000560053005F00560045005200530049004F004E005F0049004E0046004F0000000000BD04EFFE00000100000000000000000000000000000000003F000000000000000400000002000000000000000000000000000000440000000100560061007200460069006C00650049006E0066006F00000000002400040000005400720061006E0073006C006100740069006F006E00000000000000B004AC010000010053007400720069006E006700460069006C00650049006E0066006F0000008801000001003000300030003000300034006200300000002C0002000100460069006C0065004400650073006300720069007000740069006F006E000000000020000000300008000100460069006C006500560065007200730069006F006E000000000030002E0030002E0030002E00300000003C000E00010049006E007400650072006E0061006C004E0061006D00650000004400610074006100620061007300650031002E0064006C006C0000002800020001004C006500670061006C0043006F00700079007200690067006800740000002000000044000E0001004F0072006900670069006E0061006C00460069006C0065006E0061006D00650000004400610074006100620061007300650031002E0064006C006C000000340008000100500072006F006400750063007400560065007200730069006F006E00000030002E0030002E0030002E003000000038000800010041007300730065006D0062006C0079002000560065007200730069006F006E00000030002E0030002E0030002E0030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000C000000103A00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
WITH PERMISSION_SET = SAFE;
GO
CREATE FUNCTION dbo.IntervalDescription
(
    @From date, 
    @To date
)
RETURNS nvarchar(100)
AS EXTERNAL NAME 
    DBA.UserDefinedFunctions.IntervalDescription;

uso

SELECT 
    TD.FromDate,
    TD.ToDate,
    TD.ExpectedResult, 
    IntervalDescription = dbo.IntervalDescription(TD.FromDate, TD.ToDate) 
FROM dbo.TestData AS TD;

Risultato

Piano

Produzione

fonte

Non sono un programmatore C #!

using Microsoft.SqlServer.Server;
using System;
using System.Text;

public partial class UserDefinedFunctions
{
    [SqlFunction
        (
        DataAccess = DataAccessKind.None,
        SystemDataAccess = SystemDataAccessKind.None,
        IsDeterministic = true,
        IsPrecise = true,
        Name = "IntervalDescription"
        )
    ]
    [return: SqlFacet(IsFixedLength = false, IsNullable = false, MaxSize = 100)]
    public static string IntervalDescription(DateTime From, DateTime To)
    {
        var workDate = From;
        int years = To.Year - From.Year;
        int months = 0;
        int days = 0;

        if (years != 0)
        {
            if (From.Month > To.Month || (From.Month == To.Month && From.Day > To.Day))
            {
                years--;
            }
            workDate = workDate.AddYears(years);
        }

        while (workDate < To && (workDate.Year != DateTime.MaxValue.Year || workDate.Month != DateTime.MaxValue.Month))
        {
            if (workDate.AddMonths(1) <= To)
            {
                months++;
                workDate = workDate.AddMonths(1);
            }
            else
            {
                break;
            }
        }

        while (workDate < To)
        {
            days++;
            workDate = workDate.AddDays(1);
        }

        StringBuilder sb = new StringBuilder(100);

        if (years > 0)
        {
            sb.Append(years);
            sb.Append(years == 1 ? " year" : " years");
            sb.Append((months > 0 || days > 0) ? ", " : string.Empty);
        }

        if (months > 0)
        {
            sb.Append(months);
            sb.Append(months == 1 ? " month" : " months");
            sb.Append(days > 0 ? ", " : string.Empty);
        }

        if (days > 0 || (years == 0 && months == 0))
        {
            sb.Append(days);
            sb.Append(days == 1 ? " day" : " days");
        }

        return
            sb.ToString();

    }
}

8

La mia versione, implementata in SQL Server 2008R2 SP2.

CREATE FUNCTION dbo.ReadableInterval(
    @FromDate AS date,
    @ToDate AS date
)
RETURNS TABLE AS RETURN 
(
with YearStep as
(
    select
        max(n1.Number) as YearNumber
    from dbo.Numbers as n1
    where n1.Number <= DATEDIFF(YEAR, @FromDate, @ToDate)  -- see comment (A)
    and DATEADD(YEAR, n1.Number, @FromDate) <= @ToDate     -- see comment (B)
)
, MonthStep as
(
    select
        max(n2.Number) as MonthNumber
    from dbo.Numbers as n2
    cross apply YearStep as y1
    where n2.Number <= DATEDIFF(MONTH, DATEADD(YEAR, y1.YearNumber, @FromDate), @ToDate)
    and DATEADD(MONTH, n2.Number, DATEADD(YEAR, y1.YearNumber, @FromDate)) <= @ToDate
)
, DayStep as
(
    select
        DATEDIFF(day, DATEADD(MONTH, m1.MonthNumber, DATEADD(YEAR, y2.YearNumber, @FromDate)), @ToDate) as DayNumber
    from MonthStep as m1
    cross apply YearStep as y2
)
select
    y.YearNumber,
    m.MonthNumber,
    d.DayNumber
from YearStep as y
cross apply MonthStep as m
cross apply DayStep as d
)

Con i dati di test forniti i risultati sono

select
    td.FromDate,
    td.ToDate,
    td.ExpectedResult,
    ri.YearNumber as Years,
    ri.MonthNumber as Months,
    ri.DayNumber as [Days]
from dbo.TestData as td
cross apply dbo.ReadableInterval(td.FromDate, td.ToDate) as ri;
FromDate   ToDate     ExpectedResult               Years Months Days
---------- ---------- ---------------------------- ----- ------ ----
1999-12-31 1999-12-31 0 days                           0      0    0
1999-12-31 2000-01-01 1 day                            0      0    1
2000-01-01 2000-02-01 1 month                          0      1    0
2000-02-01 2000-03-01 1 month                          0      1    0
2000-01-28 2000-02-29 1 month, 1 day                   0      1    1
2000-01-01 2000-12-31 11 months, 30 days               0     11   30
2000-02-28 2000-03-01 2 days                           0      0    2
2001-02-28 2001-03-01 1 day                            0      0    1
2000-01-01 2001-01-01 1 year                           1      0    0
2000-01-01 2011-01-01 11 years                        11      0    0
9999-12-30 9999-12-31 1 day                            0      0    1
1900-01-01 9999-12-31 8099 years 11 months 30 days  8099     11   30

Spiegazione

Il mio approccio generale è di fare un passo avanti rispetto alla data precedente, prima in anni, poi in mesi, poi in giorni. Ad ogni livello di granularità l'obiettivo è avvicinarsi alla data di fine senza superarla, quindi passare al livello inferiore successivo.

Uso una tabella numerica per facilitare il calcolo vicino ma non eccessivo. Da questa tabella e DATEADDposso trovare il maggior numero di anni / mesi / giorni che precedono il ToDatecommento (B) nel codice.

Dato che stavo cercando il numero MAX e la mia tabella Numbers è raggruppata su di esso, l'ottimizzatore stava eseguendo una scansione decrescente, fornendo valori a DATEADD. Ciò stava causando errori di overflow della data poiché Numbers contiene oltre 100.000 righe. DATEADD(YEAR, 100000, @FromDate)è maggiore di 9999-12-31 e viene generato un errore. Il predicato (A) fornisce un limite superiore al valore numerico da cui inizia la scansione all'indietro, evitando l'overflow della data. Di conseguenza, il piano di query attraversa pochissime righe per intervalli di date anche molto grandi.

Questo approccio viene utilizzato per trovare anni e mesi, tranne per il fatto che il punto di partenza per mesi è portato avanti da quanti anni ho trovato nel primo CTE. GIORNI è il mio livello più basso di granularità, quindi è sufficiente un semplice DATEDIFF.

Ciò potrebbe essere esteso a una granularità più fine, restituendo l'intervallo in ore, minuti e secondi, se necessario.


7

PostgreSQL supporta la agefunzione pronta all'uso :

select
  FromDate,
  ToDate,
  ExpectedResult,
  age(ToDate, FromDate)
from TestData;

Questo dà il risultato desiderato, dare o prendere alcuni valori di tempo aggiuntivi.

FromDate      ToDate        ExpectedResult                  age
----------    ----------    ----------------------------    --------------------------
1999-12-31    1999-12-31    0 days                          00:00:00
1999-12-31    2000-01-01    1 day                           1 day
2000-01-01    2000-02-01    1 month                         1 mon
2000-02-01    2000-03-01    1 month                         1 mon
2000-01-28    2000-02-29    1 month, 1 day                  1 mon 1 day
2000-01-01    2000-12-31    11 months, 30 days              11 mons 30 days
2000-02-28    2000-03-01    2 days                          2 days
2001-02-28    2001-03-01    1 day                           1 day
2000-01-01    2001-01-01    1 year                          1 year
2000-01-01    2011-01-01    11 years                        11 years
9999-12-30    9999-12-31    1 day                           1 day
1900-01-01    9999-12-31    8099 years 11 months 30 days    8099 years 11 mons 30 days

5

Versione senza numbertabella o conteggio richiesto. Fornisce lo stesso risultato sui dati dei test di Michael Green. Differiscono sui dati dove @FromDate > @ToDate. ReadableInterval2restituisce valori negativi contrari a null.

CREATE FUNCTION dbo.ReadableInterval2(
    @FromDate AS date,
    @ToDate AS date
)
RETURNS TABLE AS RETURN 
(with checkData as (
    select 
       fromDate = case when @FromDate > @ToDate then @ToDate else @FromDate end,
       toDate = case when @FromDate <= @ToDate then @ToDate else @FromDate end,
       k = case when @FromDate > @ToDate then -1 else 1 end
), MonthStep as (
    select k, FromDate, ToDate,
        YearNumber = x.months / 12,
        MonthNumber = x.months % 12
    from checkdata
    cross apply(
        select months = DATEDIFF(MONTH, FromDate, ToDate)
            - case when DAY(FromDate) > DAY(ToDate) then 1 else 0 end
        ) x
)
select YearNumber = k*YearNumber, 
      MonthNumber = k*MonthNumber,
      DayNumber = k*DATEDIFF(day, DATEADD(MONTH, MonthNumber, DATEADD(YEAR, YearNumber, FromDate)), ToDate) 
    from MonthStep 
)

1
Cosa c'è di sbagliato nell'avere una tabella numerica? Sono abbastanza utili per una varietà di problemi, hanno un ingombro piuttosto ridotto e spesso offrono prestazioni migliori rispetto alle alternative (CTE ricorsive, XML, ecc.).
Aaron Bertrand

3
@AaronBertrand Sono d'accordo che sono abbastanza utili. Ma proprio qui non riesco a vedere quale tabella dei numeri dei problemi aiuta a risolvere. Nessuna ricorsione, nessun XML, funzioni DATEADD scalari pure, DATEDIFF. Un po 'prolisso, può essere.
Serg

Ben fatto! Avevo preso l'ordinamento FromDate / ToDate come dato dato che è convalidato altrove, ma un buon punto ben fatto. Avere valori negativi nel risultato è un'aggiunta utile.
Michael Green,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.