¿Cómo formatea el día del mes para decir "11", "21" o "23" (indicador ordinal)?


146

Sé que esto me dará el día del mes como un número ( 11, 21, 23):

SimpleDateFormat formatDayOfMonth = new SimpleDateFormat("d");

Pero ¿cómo se formatea el día del mes para incluir un indicador ordinal , por ejemplo 11th, 21sto 23rd?


13
Como referencia, estos se llaman números ordinales: en.wikipedia.org/wiki/Ordinal_number_(linguistics) .
ocodo

55
Solo para el registro, cualquier cosa que construya la respuesta en lugar de buscar la respuesta completa en una tabla es casi imposible de localizar en otros idiomas.
Thorbjørn Ravn Andersen

2
La respuesta es de alguna manera incorrecta. Mire mi respuesta por favor.
J888

2
Comentario moderno: te recomiendo que evites la SimpleDateFormatclase. No solo está desactualizado desde hace mucho tiempo, sino que también es notoriamente problemático. Hoy tenemos mucho mejor en java.timela moderna API de fecha y hora de Java y sus DateTimeFormatter.
Ole VV

Eche un vistazo a los números API ( math.tools/api/numbers ). Tiene soporte para ordinal, cardinal, número escrito en diferentes idiomas, escrito como moneda en varios idiomas, etc.
Dors

Respuestas:


181
// https://github.com/google/guava
import static com.google.common.base.Preconditions.*;

String getDayOfMonthSuffix(final int n) {
    checkArgument(n >= 1 && n <= 31, "illegal day of month: " + n);
    if (n >= 11 && n <= 13) {
        return "th";
    }
    switch (n % 10) {
        case 1:  return "st";
        case 2:  return "nd";
        case 3:  return "rd";
        default: return "th";
    }
}

La tabla de @kaliatech es agradable, pero como se repite la misma información, abre la posibilidad de un error. Tal error realmente existe en la tabla para 7tn, 17tny 27tn(este error puede solucionarse a medida que pasa el tiempo debido a la naturaleza fluida de StackOverflow, así que verifique el historial de versiones en la respuesta para ver el error).


36
realmente se siente como un descuido en el formato de datos simple, ¿no?
stevevls

51
Diviértete internacionalizando esto. : D
Trejkaz

66
@Trejkaz Fuera del alcance de la pregunta :)
Greg Mattes

3
Esta solución solo admite inglés. Use RuleBasedNumberFormat de ICU4J para la solución localizada.
Дмитро Яковлєв

77
Ustedes están fuera de la maleza, y el tono está cambiando un poco. No hay necesidad de eso. Esta pregunta no preguntó sobre las complejidades de la internacionalización. La internacionalización es sin duda un buen tema para discutir, pero debe hacerse en otra pregunta con ese enfoque. Siempre he interpretado esta pregunta como una forma rápida y sucia de lograr esto en inglés. Por lo tanto, sugiero que si realmente no hay una buena solución en el mundo de Java que sea amigable con la internacionalización, comencemos un proyecto, por ejemplo, GitHub y creemos una buena solución. Hazme saber si estas interesado.
Greg Mattes

51

No hay nada en JDK para hacer esto.

  static String[] suffixes =
  //    0     1     2     3     4     5     6     7     8     9
     { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  //    10    11    12    13    14    15    16    17    18    19
       "th", "th", "th", "th", "th", "th", "th", "th", "th", "th",
  //    20    21    22    23    24    25    26    27    28    29
       "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  //    30    31
       "th", "st" };

 Date date = new Date();
 SimpleDateFormat formatDayOfMonth  = new SimpleDateFormat("d");
 int day = Integer.parseInt(formatDateOfMonth.format(date));
 String dayStr = day + suffixes[day];

O usando el calendario:

 Calendar c = Calendar.getInstance();
 c.setTime(date);
 int day = c.get(Calendar.DAY_OF_MONTH);
 String dayStr = day + suffixes[day];

Según los comentarios de @ thorbjørn-ravn-andersen, una tabla como esta puede ser útil al localizar:

  static String[] suffixes =
     {  "0th",  "1st",  "2nd",  "3rd",  "4th",  "5th",  "6th",  "7th",  "8th",  "9th",
       "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th",
       "20th", "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th",
       "30th", "31st" };

8
Si deja que la tabla contenga el "21º", "23º", "29º" completo, puede externalizarse y localizarse en otros idiomas. Para un software exitoso que puede convertirse en un requisito.
Thorbjørn Ravn Andersen

40
private String getCurrentDateInSpecificFormat(Calendar currentCalDate) {
    String dayNumberSuffix = getDayNumberSuffix(currentCalDate.get(Calendar.DAY_OF_MONTH));
    DateFormat dateFormat = new SimpleDateFormat(" d'" + dayNumberSuffix + "' MMMM yyyy");
    return dateFormat.format(currentCalDate.getTime());
}

private String getDayNumberSuffix(int day) {
    if (day >= 11 && day <= 13) {
        return "th";
    }
    switch (day % 10) {
    case 1:
        return "st";
    case 2:
        return "nd";
    case 3:
        return "rd";
    default:
        return "th";
    }
}

Acabo de probar su solución al omitir las comillas simples en la cadena de patrón proporcionada al constructor SimpleDateFormat. Ahora veo por qué están allí. Estoy obteniendo un java.lang.IllegalArgumentExceptiondebido a "un carácter de patrón ilegal, t".
típico danny

17

La pregunta es un poco vieja. Como esta pregunta es muy ruidosa, publico lo que resolví con el método estático como una utilidad. ¡Solo cópialo, pégalo y úsalo!

 public static String getFormattedDate(Date date){
            Calendar cal=Calendar.getInstance();
            cal.setTime(date);
            //2nd of march 2015
            int day=cal.get(Calendar.DATE);

            if(!((day>10) && (day<19)))
            switch (day % 10) {
            case 1:  
                return new SimpleDateFormat("d'st' 'of' MMMM yyyy").format(date);
            case 2:  
                return new SimpleDateFormat("d'nd' 'of' MMMM yyyy").format(date);
            case 3:  
                return new SimpleDateFormat("d'rd' 'of' MMMM yyyy").format(date);
            default: 
                return new SimpleDateFormat("d'th' 'of' MMMM yyyy").format(date);
        }
        return new SimpleDateFormat("d'th' 'of' MMMM yyyy").format(date);
    }

Para probar purose

Ejemplo: ¡llamarlo desde el método principal!

Date date = new Date();
        Calendar cal=Calendar.getInstance();
        cal.setTime(date);
        for(int i=0;i<32;i++){
          System.out.println(getFormattedDate(cal.getTime()));
          cal.set(Calendar.DATE,(cal.getTime().getDate()+1));
        }

Salida:

22nd of February 2018
23rd of February 2018
24th of February 2018
25th of February 2018
26th of February 2018
27th of February 2018
28th of February 2018
1st of March 2018
2nd of March 2018
3rd of March 2018
4th of March 2018
5th of March 2018
6th of March 2018
7th of March 2018
8th of March 2018
9th of March 2018
10th of March 2018
11th of March 2018
12th of March 2018
13th of March 2018
14th of March 2018
15th of March 2018
16th of March 2018
17th of March 2018
18th of March 2018
19th of March 2018
20th of March 2018
21st of March 2018
22nd of March 2018
23rd of March 2018
24th of March 2018
25th of March 2018

16
String ordinal(int num)
{
    String[] suffix = {"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"};
    int m = num % 100;
    return String.valueOf(num) + suffix[(m > 3 && m < 21) ? 0 : (m % 10)];
}

15

Me gustaría aportar la respuesta moderna. La SimpleDateFormatclase estaba bien cuando se hizo la pregunta hace 8 años, pero debe evitarla ahora, ya que no solo está desactualizada, sino que también es notoriamente problemática. Usar en su java.timelugar.

Editar

DateTimeFormatterBuilder.appendText(TemporalField, Map<Long, String>)Es genial para este propósito. Al usarlo, creamos un formateador que hace el trabajo por nosotros:

    Map<Long, String> ordinalNumbers = new HashMap<>(42);
    ordinalNumbers.put(1L, "1st");
    ordinalNumbers.put(2L, "2nd");
    ordinalNumbers.put(3L, "3rd");
    ordinalNumbers.put(21L, "21st");
    ordinalNumbers.put(22L, "22nd");
    ordinalNumbers.put(23L, "23rd");
    ordinalNumbers.put(31L, "31st");
    for (long d = 1; d <= 31; d++) {
        ordinalNumbers.putIfAbsent(d, "" + d + "th");
    }

    DateTimeFormatter dayOfMonthFormatter = new DateTimeFormatterBuilder()
            .appendText(ChronoField.DAY_OF_MONTH, ordinalNumbers)
            .appendPattern(" MMMM")
            .toFormatter();

    LocalDate date = LocalDate.of(2018, Month.AUGUST, 30);
    for (int i = 0; i < 6; i++) {
        System.out.println(date.format(dayOfMonthFormatter));
        date = date.plusDays(1);
    }

El resultado de este fragmento es:

30th August
31st August
1st September
2nd September
3rd September
4th September

Vieja respuesta

Este código es más corto, pero en mi humilde opinión, no es tan elegante.

    // ordinal indicators by numbers (1-based, cell 0 is wasted)
    String[] ordinalIndicators = new String[31 + 1];
    Arrays.fill(ordinalIndicators, 1, ordinalIndicators.length, "th");
    ordinalIndicators[1] = ordinalIndicators[21] = ordinalIndicators[31] = "st";
    ordinalIndicators[2] = ordinalIndicators[22] = "nd";
    ordinalIndicators[3] = ordinalIndicators[23] = "rd";

    DateTimeFormatter dayOfMonthFormatter = DateTimeFormatter.ofPattern("d");

    LocalDate today = LocalDate.now(ZoneId.of("America/Menominee")).plusWeeks(1);
    System.out.println(today.format(dayOfMonthFormatter) 
                        + ordinalIndicators[today.getDayOfMonth()]);

Ejecutando este fragmento justo ahora tengo

23ro

Una de las muchas características de java.timees que es sencillo y confiable obtener el día del mes como int, lo que obviamente es necesario para elegir el sufijo correcto de la tabla.

Te recomiendo que escribas una prueba unitaria también.

PS Un formateador similar también se puede utilizar para analizar una cadena de fecha que contiene los números ordinales como 1st, 2nd, etc, que se hizo en esta pregunta: Java - Analizar con fecha de segundos opcionales .

Enlace: Tutorial de Oracle: Fecha Hora explicando cómo usar java.time.


Me encantaría obtener esta respuesta más alta ya que esta respuesta más moderna es mejor que tratar de implementar su propia implementación.
Krease

9

Si intenta conocer i18n, la solución se complica aún más.

El problema es que en otros idiomas el sufijo puede depender no solo del número en sí, sino también del nombre que cuenta. Por ejemplo, en ruso sería "2-ой день", pero "2-ая неделя" (significa "segundo día", pero "segunda semana"). Esto no se aplica si formateamos solo días, pero en un caso un poco más genérico, debe tener en cuenta la complejidad.

Creo que una buena solución (no tuve tiempo para implementar realmente) sería extender SimpleDateFormetter para aplicar MessageFormat Locale-aware antes de pasar a la clase padre. De esta forma, podría admitir, por ejemplo, los formatos de marzo% M para obtener "3-rd",% MM para obtener "03-rd" y% MMM para obtener "tercero". Desde fuera de esta clase se ve como SimpleDateFormatter normal, pero admite más formatos. Además, si este patrón fuera aplicado por error por SimpleDateFormetter regular, el resultado tendría un formato incorrecto, pero aún sería legible.


Buen punto sobre los géneros en ruso, pero eso técnicamente haría imposible% MMM sin contexto adicional.
Físico loco

@Mad Physicist, esto no es cierto ya que% MMM se aplicará al mes, por lo que sabemos el sustantivo para conjugar.
CAB

6

Muchos de los ejemplos aquí no funcionarán para 11, 12, 13. Esto es más genérico y funcionará para todos los casos.

switch (date) {
                case 1:
                case 21:
                case 31:
                    return "" + date + "st";

                case 2:
                case 22:
                    return "" + date + "nd";

                case 3:
                case 23:
                    return "" + date + "rd";

                default:
                    return "" + date + "th";
}

5

No puedo estar satisfecho con las respuestas que piden una solución solo en inglés basada en formatos manuales. He estado buscando una solución adecuada por un tiempo y finalmente la encontré.

Debería estar utilizando RuleBasedNumberFormat . Funciona perfectamente y es respetuoso con la configuración regional.


3

Hay una manera más simple y segura de hacer esto. La función que necesitará usar es getDateFromDateString (dateString); Básicamente elimina el st / nd / rd / th de una cadena de fecha y simplemente lo analiza. Puede cambiar su SimpleDateFormat a cualquier cosa y esto funcionará.

public static final SimpleDateFormat sdf = new SimpleDateFormat("d");
public static final Pattern p = Pattern.compile("([0-9]+)(st|nd|rd|th)");

private static Date getDateFromDateString(String dateString) throws ParseException {
     return sdf.parse(deleteOrdinal(dateString));
}

private static String deleteOrdinal(String dateString) {
    Matcher m = p.matcher(dateString);
    while (m.find()) {
        dateString = dateString.replaceAll(Matcher.quoteReplacement(m.group(0)), m.group(1));
    }
    return dateString;

}


1
Esta respuesta se trata de analizar una cadena mientras que la pregunta se trata de generar una cadena. Pero sigue siendo apropiado, ya que es probable que uno necesite ambas direcciones. Además, esta respuesta resuelve esta otra pregunta .
Basil Bourque

3

El único problema con la solución provista por Greg es que no representa el número mayor que 100 con los números "adolescentes" que terminan. Por ejemplo, 111 debería ser 111, no 111. Esta es mi solución:

/**
 * Return ordinal suffix (e.g. 'st', 'nd', 'rd', or 'th') for a given number
 * 
 * @param value
 *           a number
 * @return Ordinal suffix for the given number
 */
public static String getOrdinalSuffix( int value )
{
    int hunRem = value % 100;
    int tenRem = value % 10;

    if ( hunRem - tenRem == 10 )
    {
        return "th";
    }
    switch ( tenRem )
    {
    case 1:
        return "st";
    case 2:
        return "nd";
    case 3:
        return "rd";
    default:
        return "th";
    }
}

66
¿En qué caso una secuencia Día Mes tendría más de 31 días?
SatanEnglish

@SatanEnglish, lo bueno de este método de fábrica estático es que puede usarse para algo más que obtener el sufijo de un mes. :)
Jared Rummler

1
Este método devuelve st para 11, nd para 12 y rd para 13
TheIT

@SatanEnglish. Dado que hoy es el 137 de febrero en mi calendario de elección, creo que su pregunta se responde sola. En serio, abundan los calendarios no gregorianos si sabes dónde buscar.
Físico loco

No, @ TheIT, no lo hace. Probé. Regresa thpara 11, 12 y 13 como debería. Creo que el if ( hunRem - tenRem == 10 )caso se asegura de que lo haga.
Ole VV

2

RuleBasedNumberFormat en la biblioteca de la UCI

Aprecié el enlace a la biblioteca del proyecto ICU de @ Pierre-Olivier Dybman ( http://www.icu-project.org/apiref/icu4j/com/ibm/icu/text/RuleBasedNumberFormat.html ), sin embargo, aún tenía que calcular cómo usarlo, por lo que a continuación se muestra un ejemplo del RuleBasedNumberFormatuso.

Solo formateará un número único en lugar de la fecha completa, por lo que deberá crear una cadena combinada si busca una fecha en formato: lunes 3 de febrero, por ejemplo.

El siguiente código configura el RuleBasedNumberFormatformato Ordinal para una configuración regional determinada, crea un java.time ZonedDateTimey luego formatea el número con su ordinal en una cadena.

RuleBasedNumberFormat numOrdinalFormat = new RuleBasedNumberFormat(Locale.UK,
    RuleBasedNumberFormat.ORDINAL);
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Pacific/Auckland"));

String dayNumAndOrdinal = numOrdinalFormat.format(zdt.toLocalDate().getDayOfMonth());

Salida de ejemplo:

3ro

O

4to

etc.


1

Aquí hay un enfoque que actualiza un patrón DateTimeFormatter con el sufijo literal correcto si encuentra el patrón d'00', por ejemplo, para el día del mes 1 se reemplazaría por d'st'. Una vez que se ha actualizado el patrón, se puede alimentar al DateTimeFormatter para hacer el resto.

private static String[] suffixes = {"th", "st", "nd", "rd"};

private static String updatePatternWithDayOfMonthSuffix(TemporalAccessor temporal, String pattern) {
    String newPattern = pattern;
    // Check for pattern `d'00'`.
    if (pattern.matches(".*[d]'00'.*")) {
        int dayOfMonth = temporal.get(ChronoField.DAY_OF_MONTH);
        int relevantDigits = dayOfMonth < 30 ? dayOfMonth % 20 : dayOfMonth % 30;
        String suffix = suffixes[relevantDigits <= 3 ? relevantDigits : 0];
        newPattern = pattern.replaceAll("[d]'00'", "d'" + suffix + "'");
    }

    return newPattern;
}

Requiere que el patrón original se actualice justo antes de cada llamada de formateo, p. Ej.

public static String format(TemporalAccessor temporal, String pattern) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(updatePatternWithDayOfMonthSuffix(temporal, pattern));
    return formatter.format(temporal);
}

Por lo tanto, esto es útil si el patrón de formato se define fuera del código Java, por ejemplo, una plantilla, donde como si pudiera definir el patrón en Java, entonces la respuesta de @ OleV.V. podría ser más apropiado


1
Creativo y enrevesado. Ciertamente me llevó mucho tiempo entender cómo funciona. Esa no es una señal de buen código.
Ole VV

1
@ OleV.V. Gracias por los comentarios: lo he reestructurado un poco, por lo que es un poco menos detallado. ¡Acabo de ver tu enfoque y me gusta! Creo que ambos enfoques son válidos con diferentes compensaciones. La suya no requiere ningún soporte adicional en el momento del formateo, pero sí requiere que el patrón se defina utilizando el generador que impide la definición del patrón en código que no sea Java. Mi enfoque requiere soporte adicional en el punto de formateo, pero no depende del constructor para crear el patrón, por lo que es un poco más flexible donde se puede definir el patrón. Mis requisitos dictaron esto último
tazmaniax

3
Muy buena cuenta de pros y contras. ¿Es posible que desee incluirlo en la respuesta? Solo una idea.
Ole VV

1

Me escribí un método de ayuda para obtener patrones para esto.

public static String getPattern(int month) {
    String first = "MMMM dd";
    String last = ", yyyy";
    String pos = (month == 1 || month == 21 || month == 31) ? "'st'" : (month == 2 || month == 22) ? "'nd'" : (month == 3 || month == 23) ? "'rd'" : "'th'";
    return first + pos + last;
}

y luego podemos llamarlo como

LocalDate localDate = LocalDate.now();//For reference
int month = localDate.getDayOfMonth();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(getPattern(month));
String date = localDate.format(formatter);
System.out.println(date);

la salida es

December 12th, 2018

Sin embargo, esto requiere un API mínimo de 26 :)
Mikkel Larsen

@MikkelLarsen esta pregunta no era sobre Android, estaba usando Java 8 para esto. Android no admite una API grande para Java 8.
SamHoque

@SamSakerz my bad :)
Mikkel Larsen

@MikkelLarsen La mayor parte de la funcionalidad java.time se transfiere a Java 6 y Java 7 en elproyecto ThreeTen-Backport . Más adaptado para Android anterior (<26) en ThreeTenABP . Ver Cómo utilizar ThreeTenABP ... .
Basil Bourque

1

Pruebe a continuación la función:

public static String getFormattedDate(Date date) 
{
  Calendar cal = Calendar.getInstance();
  cal.setTime(date);
  //2nd of march 2015
  int day = cal.get(Calendar.DATE);
  if (!((day > 10) && (day < 19)))
   switch (day % 10) {
    case 1:
     return new SimpleDateFormat("d'st' 'of' MMMM yyyy").format(date);
    case 2:
     return new SimpleDateFormat("d'nd' 'of' MMMM yyyy").format(date);
    case 3:
     return new SimpleDateFormat("d'rd' 'of' MMMM yyyy").format(date);
    default:
     return new SimpleDateFormat("d'th' 'of' MMMM yyyy").format(date);
   }
  return new SimpleDateFormat("d'th' 'of' MMMM yyyy").format(date);
}

Al responder una pregunta anterior, su respuesta sería mucho más útil para otros usuarios de StackOverflow si incluyera algún contexto para explicar cómo ayuda su respuesta, particularmente para una pregunta que ya tiene una respuesta aceptada. Ver: ¿Cómo escribo una buena respuesta ?
David Buck

Gracias por querer contribuir. Recomiendo que no usemos Date, Calendary SimpleDateFormat. Esas clases están mal diseñadas y anticuadas, la última en particular notoriamente problemática. En su lugar, use LocalDatey DateTimeFormatter, ambos de java.time, la moderna API de fecha y hora de Java . También tengo dudas sobre qué novedades aporta tu respuesta en comparación con las otras 18 respuestas.
Ole VV

0

En kotlin puedes usar así

fun changeDateFormats(currentFormat: String, dateString: String): String {
        var result = ""
        try {
            val formatterOld = SimpleDateFormat(currentFormat, Locale.getDefault())
            formatterOld.timeZone = TimeZone.getTimeZone("UTC")

            var date: Date? = null

            date = formatterOld.parse(dateString)

            val dayFormate = SimpleDateFormat("d", Locale.getDefault())
            var day = dayFormate.format(date)

            val formatterNew = SimpleDateFormat("hh:mm a, d'" + getDayOfMonthSuffix(day.toInt()) + "' MMM yy", Locale.getDefault())

            if (date != null) {
                result = formatterNew.format(date)
            }

        } catch (e: ParseException) {
            e.printStackTrace()
            return dateString
        }

        return result
    }


    private fun getDayOfMonthSuffix(n: Int): String {
        if (n in 11..13) {
            return "th"
        }
        when (n % 10) {
            1 -> return "st"
            2 -> return "nd"
            3 -> return "rd"
            else -> return "th"
        }
    }

establecido así

  txt_chat_time_me.text = changeDateFormats("SERVER_DATE", "DATE")

-3

El siguiente método se puede usar para obtener la cadena formateada de la fecha que se le pasa. Formateará la fecha para decir primero, segundo, tercero, cuarto ... usando SimpleDateFormat en Java. por ejemplo: - 1 de septiembre de 2015

public String getFormattedDate(Date date){
            Calendar cal=Calendar.getInstance();
            cal.setTime(date);
            //2nd of march 2015
            int day=cal.get(Calendar.DATE);

            switch (day % 10) {
            case 1:  
                return new SimpleDateFormat("d'st' 'of' MMMM yyyy").format(date);
            case 2:  
                return new SimpleDateFormat("d'nd' 'of' MMMM yyyy").format(date);
            case 3:  
                return new SimpleDateFormat("d'rd' 'of' MMMM yyyy").format(date);
            default: 
                return new SimpleDateFormat("d'th' 'of' MMMM yyyy").format(date);
        }

-4

La siguiente es una respuesta más eficiente a la pregunta en lugar de codificar el estilo.

Para cambiar el día al número ordinal, debe usar el siguiente sufijo .

DD +     TH = DDTH  result >>>> 4TH

OR to spell the number add SP to the format

DD + SPTH = DDSPTH   result >>> FOURTH

Encuentra mi respuesta completa en esta pregunta.


La pregunta es formatear en Java, no en la base de datos Oracle docs.oracle.com/cd/B12037_01/server.101/b10759/… Java usa SimpleDateFormat para la fecha: docs.oracle.com/javase/tutorial/i18n/format/…
Belal mazlom

-9
public String getDaySuffix(int inDay)
{
  String s = String.valueOf(inDay);

  if (s.endsWith("1"))
  {
    return "st";
  }
  else if (s.endsWith("2"))
  {
    return "nd";
  }
  else if (s.endsWith("3"))
  {
    return "rd";
  }
  else
  {
    return "th";
  }
}

No debe usar endsWith (); que producen resultados incorrectos
Rahul Sharma
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.