Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29fdd09c0f | ||
|
|
a1934d6cd5 | ||
|
|
26627272a9 | ||
|
|
0c4cc6c992 | ||
|
|
a5b730a900 | ||
|
|
4afc03bece | ||
|
|
e236cbb164 | ||
|
|
927b10df80 | ||
|
|
bbe04b4ab9 | ||
|
|
9153d73f2b | ||
|
|
6d4811ad1c | ||
|
|
94326a3544 | ||
|
|
ef527cad43 | ||
|
|
e91b1fa66b | ||
|
|
863e66884d | ||
|
|
54516c5684 | ||
|
|
08d2bb170d | ||
|
|
c2a0d8ead6 | ||
|
|
a16517edfb | ||
|
|
26136e53c6 | ||
|
|
6654f1b922 | ||
|
|
e30f8df8fe | ||
|
|
2fe5b77498 | ||
|
|
f1a5e6a0b1 | ||
|
|
fadf5c6519 | ||
|
|
e975613c9d | ||
|
|
b1a1dd04a5 | ||
|
|
e4e2f1a79e | ||
|
|
138b442e07 | ||
|
|
119580f544 | ||
|
|
9fd97c4663 | ||
|
|
78cee0e384 | ||
|
|
fb0379287c | ||
|
|
def65ab120 | ||
|
|
dc8cfcd4c9 | ||
|
|
84c8d23710 | ||
|
|
36c3f7594d | ||
|
|
bdaffc380b | ||
|
|
5d79b34cb5 | ||
|
|
14cddf357b | ||
|
|
92eee10bcf | ||
|
|
9b7b7d655d | ||
|
|
f40aa10436 | ||
|
|
c521bdb91a | ||
|
|
bdbdf52122 | ||
|
|
7ff97fb930 | ||
|
|
af378de03f | ||
|
|
82c264bc36 | ||
|
|
4fbed9fe12 | ||
|
|
1dbf121a78 | ||
|
|
694b9b0218 | ||
|
|
0a3df5068c | ||
|
|
9e177fd592 | ||
|
|
8422af4a2e | ||
|
|
b79c5a3b4c | ||
|
|
3bb0a8379d | ||
|
|
fe0d57c4da | ||
|
|
727a958c48 | ||
|
|
aa93d654ab | ||
|
|
f5b71a79b2 | ||
|
|
173fc52610 | ||
|
|
7f7a8bca99 | ||
|
|
0929a431d4 | ||
|
|
f1e5d770ea | ||
|
|
bceda78a42 | ||
|
|
6d829120c3 | ||
|
|
785c29210e | ||
|
|
7db2daea72 | ||
|
|
0fcee760d8 | ||
|
|
e63b0b6217 | ||
|
|
9e195f5251 | ||
|
|
60320941aa | ||
|
|
871e737583 | ||
|
|
3785e6f71b | ||
|
|
a89f8a9913 | ||
|
|
93bfbdfc4e | ||
|
|
c032839468 | ||
|
|
52cf991302 | ||
|
|
0c6b44798b | ||
|
|
4c12bfcb14 | ||
|
|
fde96ce8dc | ||
|
|
bc40b6ebb5 | ||
|
|
3a34f243b8 | ||
|
|
f322d26b7d | ||
|
|
dcaa468c47 | ||
|
|
cc2d05654f | ||
|
|
c22f7343f0 | ||
|
|
858835f9f7 | ||
|
|
367d906403 | ||
|
|
df5bc5abc8 | ||
|
|
1ca5d789bb | ||
|
|
aab3543dc3 | ||
|
|
9b254e6830 | ||
|
|
0dd933b0a9 | ||
|
|
9fd85b06dc | ||
|
|
ec81110b3e | ||
|
|
094a76d514 | ||
|
|
7d737bc5ec | ||
|
|
3eb074282a | ||
|
|
b8732bd248 | ||
|
|
2252a8ff55 | ||
|
|
9a9cf419a1 | ||
|
|
0f92512030 | ||
|
|
e0806f9d6c | ||
|
|
5db9d55926 | ||
|
|
3cb3d41368 | ||
|
|
a3a081c949 | ||
|
|
207350fb44 | ||
|
|
6f7a68c9e3 | ||
|
|
0936fb45d5 | ||
|
|
bccfca4ffe | ||
|
|
417255fe1b |
+3
-9
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
[
|
[
|
||||||
inputs: [
|
inputs: [
|
||||||
"lib/*/{lib,scripts,unicode,test}/**/*.{ex,exs}",
|
"lib/*/{lib,scripts,unicode,test}/**/*.{ex,exs}",
|
||||||
@@ -17,9 +13,7 @@
|
|||||||
assert_same: 2,
|
assert_same: 2,
|
||||||
|
|
||||||
# Errors tests
|
# Errors tests
|
||||||
assert_eval_raise: 3,
|
assert_eval_raise: 3
|
||||||
|
],
|
||||||
# Float tests
|
normalize_bitstring_modifiers: false
|
||||||
float_assert: 1
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||||
*.ex diff=elixir
|
*.ex diff=elixir
|
||||||
*.exs diff=elixir
|
*.exs diff=elixir
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
---
|
---
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
---
|
---
|
||||||
name: Report an issue
|
name: Report an issue
|
||||||
description: Tell us about something that is not working the way we (probably) intend
|
description:
|
||||||
|
Tell us about something that is not working the way we (probably) intend
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -12,17 +10,11 @@ body:
|
|||||||
|
|
||||||
|
|
||||||
Please, do not use this form for guidance, questions or support.
|
Please, do not use this form for guidance, questions or support.
|
||||||
Try instead in [Elixir Forum](https://elixirforum.com) or any of
|
Try instead in [Elixir Forum](https://elixirforum.com),
|
||||||
our online communities (Slack, Discord, etc).
|
the [IRC Chat](https://web.libera.chat/#elixir),
|
||||||
|
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
|
||||||
- type: checkboxes
|
[Slack](https://elixir-slackin.herokuapp.com),
|
||||||
id: existing-issue
|
[Discord](https://discord.gg/elixir) or in other online communities.
|
||||||
attributes:
|
|
||||||
label: Existing issue
|
|
||||||
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
|
|
||||||
options:
|
|
||||||
- label: I have searched existing issues and could not find a duplicate.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: elixir-and-otp-version
|
id: elixir-and-otp-version
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
* Describe here the reasons behind the pull request.
|
|
||||||
* Make sure you have read the CONTRIBUTING.md file.
|
|
||||||
* Make sure any relevant documentation and tests have been added/updated.
|
|
||||||
* Do not submit Draft pull requests unless previously asked/agreed.
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
name: builds.hex.pm
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- v*.*
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
env:
|
||||||
|
ELIXIR_OPTS: "--warnings-as-errors"
|
||||||
|
ERLC_OPTS: "warnings_as_errors"
|
||||||
|
LANG: C.UTF-8
|
||||||
|
|
||||||
|
concurrency: builds_txt
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release_pre_built:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- otp: 24
|
||||||
|
otp_version: '24.3'
|
||||||
|
upload_generic_version: upload_generic_version
|
||||||
|
- otp: 25
|
||||||
|
otp_version: '25.3'
|
||||||
|
- otp: 26
|
||||||
|
otp_version: '26.0'
|
||||||
|
build_docs: build_docs
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
- name: Get tags
|
||||||
|
run: git fetch --tags origin
|
||||||
|
- uses: ./.github/workflows/release_pre_built
|
||||||
|
with:
|
||||||
|
otp_version: ${{ matrix.otp_version }}
|
||||||
|
otp: ${{ matrix.otp }}
|
||||||
|
build_docs: ${{ matrix.build_docs }}
|
||||||
|
- name: Utils.sh
|
||||||
|
run: |
|
||||||
|
cat << 'EOF' > utils.sh
|
||||||
|
function purge_key() {
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
-H "Fastly-Key: ${FASTLY_KEY}" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Content-Length: 0" \
|
||||||
|
"https://api.fastly.com/service/$1/purge/$2"
|
||||||
|
}
|
||||||
|
function purge() {
|
||||||
|
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||||
|
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||||
|
sleep 2
|
||||||
|
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||||
|
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||||
|
sleep 2
|
||||||
|
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||||
|
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chmod +x utils.sh
|
||||||
|
- name: Upload Docs to S3
|
||||||
|
if: ${{ matrix.build_docs }}
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||||
|
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||||
|
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||||
|
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||||
|
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||||
|
run: |
|
||||||
|
source utils.sh
|
||||||
|
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||||
|
for f in doc/*; do
|
||||||
|
if [ -d "$f" ]; then
|
||||||
|
app=`echo $f | sed s/"doc\/"//`
|
||||||
|
tarball="${app}-${version}.tar.gz"
|
||||||
|
surrogate_key="docs/${app}-${version}"
|
||||||
|
tar -czf "${tarball}" -C "doc/${app}" .
|
||||||
|
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
|
||||||
|
--cache-control "public,max-age=3600" \
|
||||||
|
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||||
|
purge "${surrogate_key}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
- name: Upload Precompiled to S3
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||||
|
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||||
|
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||||
|
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||||
|
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||||
|
run: |
|
||||||
|
source utils.sh
|
||||||
|
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||||
|
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
|
||||||
|
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
|
||||||
|
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||||
|
purge builds/elixir/${{github.ref_name}}
|
||||||
|
fi
|
||||||
|
- name: Update builds txt
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||||
|
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||||
|
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||||
|
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||||
|
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||||
|
run: |
|
||||||
|
source utils.sh
|
||||||
|
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
|
||||||
|
ref_name=${{ github.ref_name }}
|
||||||
|
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
|
||||||
|
touch builds.txt
|
||||||
|
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
|
||||||
|
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||||
|
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
|
||||||
|
sed -i "/^${ref_name} /d" builds.txt
|
||||||
|
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||||
|
fi
|
||||||
|
sort -u -k1,1 -o builds.txt builds.txt
|
||||||
|
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
|
||||||
|
purge builds/elixir/txt
|
||||||
+49
-79
@@ -1,17 +1,17 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- 'lib/**/*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
paths-ignore:
|
||||||
|
- 'lib/**/*.md'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||||
|
ERLC_OPTS: "warnings_as_errors"
|
||||||
LANG: C.UTF-8
|
LANG: C.UTF-8
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -19,133 +19,103 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_linux:
|
test_linux:
|
||||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- otp_version: "29.0"
|
- otp_version: '26.0'
|
||||||
deterministic: true
|
otp_latest: true
|
||||||
- otp_version: "28.4"
|
- otp_version: '25.3'
|
||||||
docs: true
|
- otp_version: '25.0'
|
||||||
coverage: true
|
- otp_version: '24.3'
|
||||||
- otp_version: "28.1"
|
- otp_version: '24.0'
|
||||||
- otp_version: "27.3"
|
|
||||||
- otp_version: "27.0"
|
|
||||||
- otp_version: master
|
- otp_version: master
|
||||||
development: true
|
development: true
|
||||||
- otp_version: maint
|
- otp_version: maint
|
||||||
development: true
|
development: true
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
env:
|
|
||||||
ERLC_OPTS: "warnings_as_errors"
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
fetch-depth: 50
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
|
||||||
with:
|
with:
|
||||||
otp-version: ${{ matrix.otp_version }}
|
otp-version: ${{ matrix.otp_version }}
|
||||||
|
|
||||||
- name: Set ERL_COMPILER_OPTIONS
|
|
||||||
if: ${{ matrix.deterministic }}
|
|
||||||
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Compile Elixir
|
- name: Compile Elixir
|
||||||
run: |
|
run: |
|
||||||
make compile
|
make compile
|
||||||
echo "$PWD/bin" >> $GITHUB_PATH
|
echo "$PWD/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Build info
|
- name: Build info
|
||||||
run: bin/elixir --version
|
run: bin/elixir --version
|
||||||
|
|
||||||
- name: Check format
|
- name: Check format
|
||||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||||
|
|
||||||
- name: Erlang test suite
|
- name: Erlang test suite
|
||||||
run: make test_erlang
|
run: make test_erlang
|
||||||
continue-on-error: ${{ matrix.development == true }}
|
continue-on-error: ${{ matrix.development }}
|
||||||
|
|
||||||
- name: Elixir test suite
|
- name: Elixir test suite
|
||||||
run: make test_elixir
|
run: make test_elixir
|
||||||
continue-on-error: ${{ matrix.development == true }}
|
continue-on-error: ${{ matrix.development }}
|
||||||
env:
|
|
||||||
COVER: "${{ matrix.coverage }}"
|
|
||||||
|
|
||||||
- name: Build docs (ExDoc main)
|
- name: Build docs (ExDoc main)
|
||||||
if: ${{ matrix.docs }}
|
if: ${{ matrix.otp_latest }}
|
||||||
run: |
|
run: |
|
||||||
cd ..
|
cd ..
|
||||||
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
|
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
|
||||||
cd ex_doc
|
cd ex_doc
|
||||||
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
|
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
|
||||||
cd ../elixir/
|
cd ../elixir/
|
||||||
git fetch --tags
|
make docs
|
||||||
DOCS_OPTIONS="--warnings-as-errors" make docs
|
|
||||||
|
|
||||||
- name: "Calculate Coverage"
|
|
||||||
if: ${{ matrix.coverage }}
|
|
||||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
- name: "Upload Coverage Artifact"
|
|
||||||
if: ${{ matrix.coverage }}
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: TestCoverage
|
|
||||||
path: cover/*
|
|
||||||
|
|
||||||
- name: Check reproducible builds
|
- name: Check reproducible builds
|
||||||
if: ${{ matrix.deterministic }}
|
|
||||||
run: taskset 1 make check_reproducible
|
|
||||||
|
|
||||||
- name: Check git is not required
|
|
||||||
if: ${{ matrix.deterministic }}
|
|
||||||
run: |
|
run: |
|
||||||
rm -rf .git
|
rm -rf .git
|
||||||
cd lib/elixir
|
# Recompile System without .git
|
||||||
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
|
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
|
||||||
|
taskset 1 make check_reproducible
|
||||||
|
if: ${{ matrix.otp_latest }}
|
||||||
|
|
||||||
test_windows:
|
test_windows:
|
||||||
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
|
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||||
runs-on: windows-2022
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
otp_version:
|
otp_version: ['24', '25', '26']
|
||||||
- "29.0"
|
runs-on: windows-2019
|
||||||
- "28.1"
|
|
||||||
- "27.3"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Configure Git
|
- name: Configure Git
|
||||||
run: git config --global core.autocrlf input
|
run: git config --global core.autocrlf input
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
fetch-depth: 50
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
|
||||||
with:
|
with:
|
||||||
otp-version: ${{ matrix.otp_version }}
|
otp-version: ${{ matrix.otp_version }}
|
||||||
|
|
||||||
- name: Compile Elixir
|
- name: Compile Elixir
|
||||||
run: |
|
run: |
|
||||||
Remove-Item -Recurse -Force '.git'
|
Remove-Item -Recurse -Force '.git'
|
||||||
make compile
|
make compile
|
||||||
|
|
||||||
- name: Build info
|
- name: Build info
|
||||||
run: bin/elixir --version
|
run: bin/elixir --version
|
||||||
|
|
||||||
- name: Check format
|
- name: Check format
|
||||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||||
|
|
||||||
- name: Erlang test suite
|
- name: Erlang test suite
|
||||||
run: make test_erlang
|
run: make --keep-going test_erlang
|
||||||
|
|
||||||
- name: Elixir test suite
|
- name: Elixir test suite
|
||||||
run: |
|
run: |
|
||||||
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
|
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
|
||||||
make test_elixir
|
make --keep-going test_elixir
|
||||||
|
|
||||||
|
check_posix_compliant:
|
||||||
|
name: Check POSIX-compliant
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
- name: Install Shellcheck
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y shellcheck
|
||||||
|
- name: Check POSIX-compliant
|
||||||
|
run: |
|
||||||
|
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||||
|
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||||
|
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2026 The Elixir Team
|
|
||||||
|
|
||||||
name: "CodeQL Advanced"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
schedule:
|
|
||||||
- cron: "29 8 * * 1"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze (${{ matrix.language }})
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: actions
|
|
||||||
build-mode: none
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
build-mode: ${{ matrix.build-mode }}
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
|
|
||||||
zizmor:
|
|
||||||
name: Zizmor
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Run zizmor
|
|
||||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
name: License Compliance
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
LANG: C.UTF-8
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
license_compliance:
|
|
||||||
name: Check License Compliance
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Use HTTPS instead of SSH for Git cloning
|
|
||||||
id: git-config
|
|
||||||
shell: bash
|
|
||||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
|
||||||
|
|
||||||
- name: Checkout project
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run OSS Review Toolkit
|
|
||||||
id: ort
|
|
||||||
uses: ./.github/workflows/ort
|
|
||||||
with:
|
|
||||||
upload-reports: true
|
|
||||||
fail-on-violation: true
|
|
||||||
report-formats: "WebApp"
|
|
||||||
version: "${{ github.sha }}"
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
name: Markdown Content
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "main"
|
|
||||||
|
|
||||||
paths: &paths-filter
|
|
||||||
- "**/*.md"
|
|
||||||
- .github/workflows/markdown.yml
|
|
||||||
- .markdownlint-cli2.jsonc
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths: *paths-filter
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
LANG: C.UTF-8
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint Markdown content
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run markdownlint-cli2
|
|
||||||
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
# #!/usr/bin/env elixir
|
# #!/usr/bin/env elixir
|
||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
[tag] = System.argv()
|
[tag] = System.argv()
|
||||||
|
|
||||||
Mix.install([
|
Mix.install([
|
||||||
@@ -74,6 +70,6 @@ unless System.get_env("DRYRUN") do
|
|||||||
"api-username" => "Elixir"
|
"api-username" => "Elixir"
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers)
|
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||||
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Notify
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
name: Notify
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
|
with:
|
||||||
|
otp-version: '25.0'
|
||||||
|
elixir-version: '1.14.0'
|
||||||
|
- name: Run Elixir script
|
||||||
|
env:
|
||||||
|
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||||
|
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||||
|
run: |
|
||||||
|
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
name: "Run OSS Review Toolkit"
|
|
||||||
description: "Runs OSS Review Toolkit & generates SBoMs"
|
|
||||||
inputs:
|
|
||||||
report-formats:
|
|
||||||
description: "ORT Report Formats"
|
|
||||||
required: true
|
|
||||||
fail-on-violation:
|
|
||||||
description: "Whether to fail on violation."
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
upload-reports:
|
|
||||||
description: "Whether to upload all reports"
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
version:
|
|
||||||
description: "Elixir Version (Tag / SHA)"
|
|
||||||
required: true
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
results-path:
|
|
||||||
description: "See oss-review-toolkit/ort-ci-github-action action"
|
|
||||||
value: "${{ steps.ort.outputs.results-path }}"
|
|
||||||
results-sbom-cyclonedx-xml-path:
|
|
||||||
description: "See oss-review-toolkit/ort-ci-github-action action"
|
|
||||||
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
|
|
||||||
results-sbom-cyclonedx-json-path:
|
|
||||||
description: "See oss-review-toolkit/ort-ci-github-action action"
|
|
||||||
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
|
|
||||||
results-sbom-spdx-yml-path:
|
|
||||||
description: "See oss-review-toolkit/ort-ci-github-action action"
|
|
||||||
value: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
|
|
||||||
results-sbom-spdx-json-path:
|
|
||||||
description: "See oss-review-toolkit/ort-ci-github-action action"
|
|
||||||
value: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Fetch Default ORT Config
|
|
||||||
id: fetch-default-ort-config
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
repository: oss-review-toolkit/ort-config
|
|
||||||
ref: "main"
|
|
||||||
path: ".ort-config"
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup ORT Config
|
|
||||||
id: setup-ort-config
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p "/$HOME/.ort/"
|
|
||||||
|
|
||||||
# Move Fetched Default Config into Place
|
|
||||||
mv .ort-config "$HOME/.ort/config"
|
|
||||||
|
|
||||||
# Append Global ORT Config
|
|
||||||
cat .ort/config/config.yml >> "$HOME/.ort/config/config.yml"
|
|
||||||
|
|
||||||
# Override Default Evaluator Rules
|
|
||||||
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
|
|
||||||
|
|
||||||
# Add Package Configurations
|
|
||||||
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
|
|
||||||
for FILE in .ort/package-configurations/*.yml; do
|
|
||||||
COMPONENT="$(basename "$FILE")"
|
|
||||||
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
|
||||||
sed -i -E \
|
|
||||||
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
|
|
||||||
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Set Version in SPDX & Config
|
|
||||||
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
|
|
||||||
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
|
|
||||||
sed -i "s|https://github.com/elixir-lang/elixir.git|${ELIXIR_REPO}@${ELIXIR_VERSION}|" project.spdx.yml
|
|
||||||
env:
|
|
||||||
ELIXIR_VERSION: "${{ inputs.version }}"
|
|
||||||
ELIXIR_REPO: "${{ github.server_url }}/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
- name: "Cache ScanCode"
|
|
||||||
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
|
|
||||||
with:
|
|
||||||
path: "~/.cache/scancode-tk"
|
|
||||||
key: ${{ runner.os }}-scancode
|
|
||||||
|
|
||||||
- name: Run OSS Review Toolkit
|
|
||||||
id: ort
|
|
||||||
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
|
|
||||||
with:
|
|
||||||
image: ghcr.io/oss-review-toolkit/ort:92.2.0
|
|
||||||
run: >-
|
|
||||||
labels,
|
|
||||||
cache-dependencies,
|
|
||||||
cache-scan-results,
|
|
||||||
analyzer,
|
|
||||||
scanner,
|
|
||||||
advisor,
|
|
||||||
evaluator,
|
|
||||||
reporter,
|
|
||||||
${{ inputs.upload-reports == 'true' && 'upload-results' || '' }}
|
|
||||||
fail-on: "${{ inputs.fail-on-violation == 'true' && 'violations,issues' || '' }}"
|
|
||||||
report-formats: "${{ inputs.report-formats }}"
|
|
||||||
ort-cli-report-args: >-
|
|
||||||
-O CycloneDX=output.file.formats=json,xml
|
|
||||||
-O SpdxDocument=outputFileFormats=JSON,YAML
|
|
||||||
ort-cli-scan-args: >-
|
|
||||||
--scanners Provenant
|
|
||||||
sw-version: "${{ inputs.version }}"
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
name: POSIX Compliance
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths: &paths-filter
|
|
||||||
- .github/workflows/posix_compliance.yml
|
|
||||||
- bin/elixir
|
|
||||||
- bin/elixirc
|
|
||||||
- bin/iex
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths: *paths-filter
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
LANG: C.UTF-8
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_posix_compliance:
|
|
||||||
name: Check POSIX compliance
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install ShellCheck
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y shellcheck
|
|
||||||
|
|
||||||
- name: Run ShellCheck on bin/ dir
|
|
||||||
run: |
|
|
||||||
shellcheck -e SC2039,2086 bin/elixir && \
|
|
||||||
echo "bin/elixir is POSIX compliant"
|
|
||||||
|
|
||||||
shellcheck bin/elixirc && \
|
|
||||||
echo "bin/elixirc is POSIX compliant"
|
|
||||||
|
|
||||||
shellcheck bin/iex && \
|
|
||||||
echo "bin/iex is POSIX compliant"
|
|
||||||
+32
-409
@@ -1,446 +1,69 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
name: Release
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
name: Releases
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- v*.*
|
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ELIXIR_OPTS: "--warnings-as-errors"
|
ELIXIR_OPTS: "--warnings-as-errors"
|
||||||
|
ERLC_OPTS: "warnings_as_errors"
|
||||||
LANG: C.UTF-8
|
LANG: C.UTF-8
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_draft_release:
|
create_draft_release:
|
||||||
name: Create draft release
|
runs-on: ubuntu-22.04
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
if: github.ref_type != 'branch'
|
|
||||||
run: |
|
run: |
|
||||||
gh release create \
|
gh release create \
|
||||||
--repo "$GITHUB_REPOSITORY" \
|
--repo ${{ github.repository }} \
|
||||||
--title "$GITHUB_REF_NAME" \
|
--title ${{ github.ref_name }} \
|
||||||
--notes '' \
|
--notes '' \
|
||||||
--draft \
|
--draft \
|
||||||
"$GITHUB_REF_NAME"
|
${{ github.ref_name }}
|
||||||
|
release_pre_built:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
needs: create_draft_release
|
||||||
# zizmor: ignore[artipacked]
|
|
||||||
if: github.ref_type == 'branch'
|
|
||||||
|
|
||||||
- name: Update ${{ github.ref_name }}-latest
|
|
||||||
if: github.ref_type == 'branch'
|
|
||||||
run: |
|
|
||||||
ref_name="${GITHUB_REF_NAME}-latest"
|
|
||||||
|
|
||||||
if ! gh release view "$ref_name"; then
|
|
||||||
gh release create \
|
|
||||||
--latest=false \
|
|
||||||
--title "$ref_name" \
|
|
||||||
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
|
|
||||||
"$ref_name"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git tag "$ref_name" --force
|
|
||||||
git push origin "$ref_name" --force
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- otp: 27
|
- otp: 24
|
||||||
otp_version: "27.0"
|
otp_version: '24.3'
|
||||||
|
- otp: 25
|
||||||
- otp: 28
|
otp_version: '25.3'
|
||||||
otp_version: "28.0"
|
- otp: 26
|
||||||
|
otp_version: '26.0'
|
||||||
build_docs: build_docs
|
build_docs: build_docs
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
- otp: 29
|
|
||||||
otp_version: "29.0"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
fetch-depth: 50
|
||||||
|
- uses: ./.github/workflows/release_pre_built
|
||||||
- name: "Build Release"
|
|
||||||
uses: ./.github/workflows/release_pre_built
|
|
||||||
with:
|
with:
|
||||||
otp_version: ${{ matrix.otp_version }}
|
otp_version: ${{ matrix.otp_version }}
|
||||||
otp: ${{ matrix.otp }}
|
otp: ${{ matrix.otp }}
|
||||||
build_docs: ${{ matrix.build_docs }}
|
build_docs: ${{ matrix.build_docs }}
|
||||||
|
- name: Upload Pre-built
|
||||||
- name: Create Docs Hashes
|
|
||||||
if: matrix.build_docs
|
|
||||||
run: |
|
|
||||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
|
||||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
|
||||||
|
|
||||||
- name: "Upload Linux release artifacts"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
|
||||||
path: elixir-otp-${{ matrix.otp }}.zip
|
|
||||||
|
|
||||||
- name: "Upload Windows release artifacts"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
|
||||||
path: elixir-otp-${{ matrix.otp }}.exe
|
|
||||||
|
|
||||||
- name: "Upload doc artifacts"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
if: matrix.build_docs
|
|
||||||
with:
|
|
||||||
name: Docs
|
|
||||||
path: Docs.zip*
|
|
||||||
|
|
||||||
sign:
|
|
||||||
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
|
|
||||||
needs: [build]
|
|
||||||
environment: release
|
|
||||||
strategy:
|
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
|
||||||
otp: [27, 28, 29]
|
|
||||||
flavor: [windows, linux]
|
|
||||||
|
|
||||||
env:
|
|
||||||
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Download build"
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
|
||||||
|
|
||||||
- name: Log in to Azure
|
|
||||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
|
||||||
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- name: "Sign files with Trusted Signing"
|
|
||||||
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
|
||||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
|
||||||
with:
|
|
||||||
endpoint: https://eus.codesigning.azure.net/
|
|
||||||
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
|
||||||
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
|
|
||||||
files-folder: ${{ github.workspace }}
|
|
||||||
files-folder-filter: exe
|
|
||||||
file-digest: SHA256
|
|
||||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
||||||
timestamp-digest: SHA256
|
|
||||||
|
|
||||||
- name: Create Release Hashes
|
|
||||||
if: matrix.flavor == 'windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
|
|
||||||
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
|
|
||||||
|
|
||||||
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
|
|
||||||
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
|
|
||||||
|
|
||||||
- name: Create Release Hashes
|
|
||||||
if: matrix.flavor == 'linux'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
|
|
||||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
|
||||||
|
|
||||||
- name: "Upload Linux release artifacts"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
|
||||||
path: ${{ env.RELEASE_FILE }}*
|
|
||||||
|
|
||||||
sbom:
|
|
||||||
name: Generate SBoM
|
|
||||||
needs: [build, sign]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Use HTTPS instead of SSH for Git cloning
|
|
||||||
id: git-config
|
|
||||||
shell: bash
|
|
||||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
|
||||||
|
|
||||||
- name: Checkout project
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: "Download Build Artifacts"
|
|
||||||
id: download-build-artifacts
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
|
||||||
merge-multiple: true
|
|
||||||
path: /tmp/build-artifacts/
|
|
||||||
|
|
||||||
- name: "Run OSS Review Toolkit"
|
|
||||||
id: ort
|
|
||||||
uses: ./.github/workflows/ort
|
|
||||||
with:
|
|
||||||
report-formats: "CycloneDx,SpdxDocument"
|
|
||||||
version: "${{ github.ref_type == 'tag' && github.ref_name || github.sha }}"
|
|
||||||
|
|
||||||
- name: Attest Distribution Assets with SBoM
|
|
||||||
id: attest-sbom
|
|
||||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
|
||||||
with:
|
|
||||||
subject-path: |
|
|
||||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
|
||||||
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
|
||||||
sbom-path: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
|
||||||
|
|
||||||
- name: "Copy SBoM provenance"
|
|
||||||
id: sbom-provenance
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir attestations
|
|
||||||
|
|
||||||
for FILE in /tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}; do
|
|
||||||
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
|
|
||||||
done
|
|
||||||
|
|
||||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
|
|
||||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
|
|
||||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
|
|
||||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
|
|
||||||
env:
|
|
||||||
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
|
|
||||||
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
|
|
||||||
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
|
|
||||||
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
|
|
||||||
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
|
||||||
|
|
||||||
- name: "Assemble Release SBoM Artifacts"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: "SBoM"
|
|
||||||
path: |
|
|
||||||
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
|
|
||||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
|
||||||
|
|
||||||
- name: "Assemble Distribution Attestations"
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: "Attestations"
|
|
||||||
path: "attestations/*.sigstore"
|
|
||||||
|
|
||||||
upload-release:
|
|
||||||
name: Upload release
|
|
||||||
needs: [create_draft_release, build, sign, sbom]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Upload Pre-build
|
|
||||||
shell: bash
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
|
gh release upload --clobber "${{ github.ref_name }}" \
|
||||||
tag="${GITHUB_REF_NAME}-latest"
|
elixir-otp-${{ matrix.otp }}.zip \
|
||||||
else
|
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
|
||||||
tag="$GITHUB_REF_NAME"
|
elixir-otp-${{ matrix.otp }}.exe \
|
||||||
fi
|
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
|
||||||
|
- name: Upload Docs to GitHub
|
||||||
gh release upload \
|
if: ${{ matrix.build_docs }}
|
||||||
--repo "$GITHUB_REPOSITORY" \
|
|
||||||
--clobber \
|
|
||||||
"$tag" \
|
|
||||||
elixir-otp-*.zip \
|
|
||||||
elixir-otp-*.zip.sha{1,256}sum \
|
|
||||||
elixir-otp-*.zip.sigstore \
|
|
||||||
elixir-otp-*.exe \
|
|
||||||
elixir-otp-*.exe.sha{1,256}sum \
|
|
||||||
elixir-otp-*.exe.sigstore \
|
|
||||||
Docs.zip \
|
|
||||||
Docs.zip.sha{1,256}sum \
|
|
||||||
Docs.zip.sigstore \
|
|
||||||
bom.*
|
|
||||||
|
|
||||||
upload-builds-hex-pm:
|
|
||||||
name: Upload builds to hex.pm
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs: [build, sign]
|
|
||||||
concurrency: builds-hex-pm
|
|
||||||
environment: release
|
|
||||||
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
|
|
||||||
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "Check if variables are set up"
|
|
||||||
if: "${{ ! vars.HEX_AWS_REGION }}"
|
|
||||||
run: |
|
|
||||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Init purge keys file
|
|
||||||
run: |
|
|
||||||
touch purge_keys.txt
|
|
||||||
|
|
||||||
- name: Upload Precompiled to S3
|
|
||||||
run: |
|
|
||||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
|
||||||
|
|
||||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
|
||||||
dest=${zip/elixir/${GITHUB_REF_NAME}}
|
|
||||||
surrogate_key=${dest/.zip$/}
|
|
||||||
|
|
||||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
|
|
||||||
--cache-control "public,max-age=3600" \
|
|
||||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
|
||||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
|
||||||
|
|
||||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
|
||||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
|
|
||||||
--cache-control "public,max-age=3600" \
|
|
||||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
|
||||||
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Upload Docs to S3
|
|
||||||
run: |
|
|
||||||
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
|
|
||||||
|
|
||||||
unzip Docs.zip
|
|
||||||
|
|
||||||
for f in doc/*; do
|
|
||||||
if [ -d "$f" ]; then
|
|
||||||
app=$(echo "$f" | sed s/"doc\/"//)
|
|
||||||
tarball="${app}-${version}.tar.gz"
|
|
||||||
surrogate_key="docs/${app}-${version}"
|
|
||||||
|
|
||||||
tar -czf "${tarball}" -C "doc/${app}" .
|
|
||||||
aws s3 cp "${tarball}" "s3://${AWS_S3_BUCKET}/docs/${tarball}" \
|
|
||||||
--cache-control "public,max-age=3600" \
|
|
||||||
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
|
||||||
echo "${surrogate_key}" >> ../purge_keys.txt
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Update builds txt
|
|
||||||
run: |
|
|
||||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
||||||
ref_name="$GITHUB_REF_NAME"
|
|
||||||
|
|
||||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
|
||||||
|
|
||||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
|
||||||
touch builds.txt
|
|
||||||
|
|
||||||
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
|
|
||||||
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
|
|
||||||
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
|
|
||||||
|
|
||||||
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
|
|
||||||
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
|
||||||
|
|
||||||
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
|
|
||||||
sed -i "/^${ref_name} /d" builds.txt
|
|
||||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
sort -u -k1,1 -o builds.txt builds.txt
|
|
||||||
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
|
|
||||||
--cache-control "public,max-age=3600" \
|
|
||||||
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
|
|
||||||
|
|
||||||
echo 'builds/elixir/txt' >> purge_keys.txt
|
|
||||||
|
|
||||||
- name: Flush cache
|
|
||||||
if: github.repository == 'elixir-lang/elixir'
|
|
||||||
run: |
|
|
||||||
function purge_key() {
|
|
||||||
curl \
|
|
||||||
-X POST \
|
|
||||||
-H "Fastly-Key: ${FASTLY_KEY}" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Length: 0" \
|
|
||||||
"https://api.fastly.com/service/$1/purge/$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
function purge() {
|
|
||||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
|
||||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
|
||||||
sleep 2
|
|
||||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
|
||||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
|
||||||
sleep 2
|
|
||||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
|
||||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in $(cat purge_keys.txt); do
|
|
||||||
purge "${key}"
|
|
||||||
done
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
run: |
|
||||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
gh release upload --clobber "${{ github.ref_name }}" \
|
||||||
|
Docs.zip \
|
||||||
|
Docs.zip.sha{1,256}sum
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
name: Release Notifications
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
notify:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Notify
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
|
||||||
with:
|
|
||||||
otp-version: "27.3"
|
|
||||||
elixir-version: "1.18.3"
|
|
||||||
|
|
||||||
- name: Run Elixir script
|
|
||||||
env:
|
|
||||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
|
||||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
|
||||||
run: |
|
|
||||||
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
|
|
||||||
@@ -1,70 +1,53 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
name: "Release pre built"
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
description: "Builds elixir release, ExDoc and generates docs"
|
||||||
|
|
||||||
name: Release Pre-build
|
|
||||||
description: "Builds Elixir release, ExDoc and generates docs"
|
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
otp:
|
otp:
|
||||||
description: "The major OTP version"
|
description: "The major OTP version"
|
||||||
|
|
||||||
otp_version:
|
otp_version:
|
||||||
description: "The exact OTP version (major.minor[.patch])"
|
description: "The exact OTP version (major.minor[.patch])"
|
||||||
|
|
||||||
build_docs:
|
build_docs:
|
||||||
description: "Whether docs have to be built"
|
description: "If docs have to be built or not"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
|
- uses: erlef/setup-beam@v1
|
||||||
with:
|
with:
|
||||||
otp-version: ${{ inputs.otp_version }}
|
otp-version: ${{ inputs.otp_version }}
|
||||||
version-type: strict
|
version-type: strict
|
||||||
|
|
||||||
- name: Build Elixir Release
|
- name: Build Elixir Release
|
||||||
shell: bash
|
shell: bash
|
||||||
run: | # zizmor: ignore[github-env]
|
run: |
|
||||||
make Precompiled.zip
|
make Precompiled.zip
|
||||||
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
|
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||||
|
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
|
||||||
|
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
|
||||||
echo "$PWD/bin" >> $GITHUB_PATH
|
echo "$PWD/bin" >> $GITHUB_PATH
|
||||||
env:
|
|
||||||
INPUT_OTP: ${{ inputs.otp }}
|
|
||||||
|
|
||||||
- name: Install NSIS
|
- name: Install NSIS
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y nsis
|
sudo apt install -y nsis
|
||||||
|
|
||||||
- name: Build Elixir Windows Installer
|
- name: Build Elixir Windows Installer
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export OTP_VERSION="$INPUT_OTP_VERSION"
|
export OTP_VERSION=${{ inputs.otp_version }}
|
||||||
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
|
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||||
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
|
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||||
env:
|
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
|
||||||
INPUT_OTP: ${{ inputs.otp }}
|
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
|
||||||
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
|
- name: Get latest stable ExDoc version
|
||||||
- name: Get ExDoc ref
|
|
||||||
if: ${{ inputs.build_docs }}
|
if: ${{ inputs.build_docs }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: | # zizmor: ignore[github-env]
|
run: |
|
||||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||||
ref=main
|
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
|
||||||
else
|
- uses: actions/checkout@v3
|
||||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
|
||||||
fi
|
|
||||||
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
if: ${{ inputs.build_docs }}
|
if: ${{ inputs.build_docs }}
|
||||||
with:
|
with:
|
||||||
repository: elixir-lang/ex_doc
|
repository: elixir-lang/ex_doc
|
||||||
ref: ${{ env.EX_DOC_REF }}
|
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
|
||||||
path: ex_doc
|
path: ex_doc
|
||||||
persist-credentials: false
|
|
||||||
- name: Build ex_doc
|
- name: Build ex_doc
|
||||||
if: ${{ inputs.build_docs }}
|
if: ${{ inputs.build_docs }}
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -77,5 +60,6 @@ runs:
|
|||||||
if: ${{ inputs.build_docs }}
|
if: ${{ inputs.build_docs }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
git fetch --tags
|
|
||||||
make Docs.zip
|
make Docs.zip
|
||||||
|
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||||
|
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||||
|
|||||||
+2
-8
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
/doc/
|
/doc/
|
||||||
/lib/*/ebin/
|
/lib/*/ebin/
|
||||||
/lib/*/_build/
|
/lib/*/_build/
|
||||||
@@ -10,10 +6,8 @@
|
|||||||
/lib/elixir/test/ebin/
|
/lib/elixir/test/ebin/
|
||||||
/man/elixir.1
|
/man/elixir.1
|
||||||
/man/iex.1
|
/man/iex.1
|
||||||
/Docs.zip
|
/Docs-v*.zip
|
||||||
/Precompiled.zip
|
/Precompiled-v*.zip
|
||||||
/.eunit
|
/.eunit
|
||||||
.elixir.plt
|
.elixir.plt
|
||||||
erl_crash.dump
|
erl_crash.dump
|
||||||
/cover/
|
|
||||||
.tool-versions
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
{
|
|
||||||
"globs": [
|
|
||||||
"**/*.md"
|
|
||||||
],
|
|
||||||
"ignores": [
|
|
||||||
".git/**",
|
|
||||||
".github/**"
|
|
||||||
],
|
|
||||||
"gitignore": true,
|
|
||||||
"config": {
|
|
||||||
// Consecutive header levels (h1 -> h2 -> h3).
|
|
||||||
"MD001": false,
|
|
||||||
// Header style. We use #s.
|
|
||||||
"MD003": {
|
|
||||||
"style": "atx"
|
|
||||||
},
|
|
||||||
// Style of unordered lists..
|
|
||||||
"MD007": {
|
|
||||||
"indent": 2,
|
|
||||||
"start_indented": true
|
|
||||||
},
|
|
||||||
// Line length. Who cares.
|
|
||||||
"MD013": false,
|
|
||||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
|
||||||
// don't show output. We use those a lot, so this is fine for us.
|
|
||||||
"MD014": false,
|
|
||||||
// Multiple headings with the same content.
|
|
||||||
"MD024": {
|
|
||||||
// Duplication is allowed for headings with different parents.
|
|
||||||
"siblings_only": true
|
|
||||||
},
|
|
||||||
// Trailing punctuation in heading.
|
|
||||||
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
|
|
||||||
// the default values.
|
|
||||||
"MD026": {
|
|
||||||
"punctuation": ".,;:。,;:!"
|
|
||||||
},
|
|
||||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
|
||||||
"MD028": false,
|
|
||||||
// Allowed HTML inline elements.
|
|
||||||
"MD033": {
|
|
||||||
"allowed_elements": [
|
|
||||||
"h1",
|
|
||||||
"a",
|
|
||||||
"br",
|
|
||||||
"img",
|
|
||||||
"picture",
|
|
||||||
"source",
|
|
||||||
"noscript",
|
|
||||||
"p",
|
|
||||||
"script"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
|
||||||
"MD038": false,
|
|
||||||
// Code block style. We don't care if it's fenced or indented.
|
|
||||||
"MD046": false,
|
|
||||||
// Our tables are too large to align.
|
|
||||||
"MD060": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
excludes:
|
|
||||||
paths:
|
|
||||||
- pattern: "man/*"
|
|
||||||
reason: "DOCUMENTATION_OF"
|
|
||||||
comment: "Documentation"
|
|
||||||
- pattern: ".github/**/*"
|
|
||||||
reason: "BUILD_TOOL_OF"
|
|
||||||
comment: "Documentation"
|
|
||||||
- pattern: ".ort/**/*"
|
|
||||||
reason: "BUILD_TOOL_OF"
|
|
||||||
comment: "Documentation"
|
|
||||||
|
|
||||||
# Unfortunately we'll have to repeat all package level excludes here
|
|
||||||
# Make sure to keep them in sync with the package configuration in
|
|
||||||
# .ort/package-configurations
|
|
||||||
- pattern: "lib/*/pages/**/*"
|
|
||||||
reason: "DOCUMENTATION_OF"
|
|
||||||
comment: "Documentation"
|
|
||||||
- pattern: "lib/*/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
- pattern: "lib/*/scripts/**/*"
|
|
||||||
reason: "BUILD_TOOL_OF"
|
|
||||||
comment: "Build Tool"
|
|
||||||
- pattern: "lib/*/examples/**/*"
|
|
||||||
reason: "EXAMPLE_OF"
|
|
||||||
comment: "Example"
|
|
||||||
|
|
||||||
curations:
|
|
||||||
license_findings:
|
|
||||||
# Version File
|
|
||||||
- path: "VERSION"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply Trademark Policy to VERSION file"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: ".github/pull_request_template.md"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to GitHub pull request template"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Wrongly Identified
|
|
||||||
- path: ".gitignore"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Ignored by ScanCode"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: ".gitattributes"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Ignored by ScanCode"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: "CONTRIBUTING.md"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Wrongly identified TSL license"
|
|
||||||
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: "OPEN_SOURCE_POLICY.md"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Wrongly identified NOASSERTION"
|
|
||||||
detected_license: "NOASSERTION"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Unfortunately we'll have to repeat all package level license curations here
|
|
||||||
# Make sure to keep them in sync with the package configuration in
|
|
||||||
# .ort/package-configurations
|
|
||||||
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/*/test/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Logos
|
|
||||||
- path: "lib/elixir/pages/images/logo.png"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply Trademark Policy to Elixir Logo"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
|
||||||
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply Trademark Policy to Elixir Logo"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
|
||||||
|
|
||||||
# Documentation Images
|
|
||||||
- path: "lib/elixir/pages/images/**/*.png"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to all images"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Unicode
|
|
||||||
- path: "lib/elixir/unicode/*.txt"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to unicode files"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-scancode-unicode"
|
|
||||||
|
|
||||||
# Wrongly Identified
|
|
||||||
- path: "lib/elixir/pages/references/library-guidelines.md"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: |
|
|
||||||
The guide mentions multiple licenses for users to choose from.
|
|
||||||
It however is not licensed itself by the mentioned licenses.
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Ignored by ScanCode"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
ort:
|
|
||||||
enableRepositoryPackageCurations: true
|
|
||||||
enableRepositoryPackageConfigurations: true
|
|
||||||
|
|
||||||
scanner:
|
|
||||||
skipConcluded: false
|
|
||||||
includeFilesWithoutFindings: true
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
allowDynamicVersions: true
|
|
||||||
enabledPackageManagers: [SpdxDocumentFile]
|
|
||||||
|
|
||||||
reporter:
|
|
||||||
reporters:
|
|
||||||
SpdxDocument:
|
|
||||||
options:
|
|
||||||
creationInfoOrganization: The Elixir Team
|
|
||||||
documentName: "Elixir Source SPDX Document"
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
|
|
||||||
* Copyright (c) 2021 The Elixir Team
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Docs: https://oss-review-toolkit.org/ort/docs/configuration/evaluator-rules
|
|
||||||
|
|
||||||
val whitelistedLicenses = listOf(
|
|
||||||
// License for Elixir & Imported Erlang Projects
|
|
||||||
"Apache-2.0",
|
|
||||||
// License for the Elixir Logo
|
|
||||||
"LicenseRef-elixir-trademark-policy",
|
|
||||||
"LicenseRef-scancode-elixir-trademark-policy",
|
|
||||||
// License for included Unicode Files
|
|
||||||
"LicenseRef-scancode-unicode",
|
|
||||||
// DCO for committers
|
|
||||||
"LicenseRef-scancode-dco-1.1"
|
|
||||||
).map { SpdxSingleLicenseExpression.parse(it) }.toSet()
|
|
||||||
|
|
||||||
fun PackageRule.howToFixDefault() = """
|
|
||||||
* Check if this license violation is intended
|
|
||||||
* Adjust evaluation rules in `.ort/config/evaluator.rules.kts`
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
fun PackageRule.LicenseRule.isHandled() =
|
|
||||||
object : RuleMatcher {
|
|
||||||
override val description = "isHandled($license)"
|
|
||||||
|
|
||||||
override fun matches() = license in whitelistedLicenses
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RuleSet.unhandledLicenseRule() = packageRule("UNHANDLED_LICENSE") {
|
|
||||||
// Do not trigger this rule on packages that have been excluded in the .ort.yml.
|
|
||||||
require {
|
|
||||||
-isExcluded()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a rule that is executed for each license of the package.
|
|
||||||
licenseRule("UNHANDLED_LICENSE", LicenseView.CONCLUDED_OR_DECLARED_AND_DETECTED) {
|
|
||||||
require {
|
|
||||||
-isExcluded()
|
|
||||||
-isHandled()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throw an error message including guidance how to fix the issue.
|
|
||||||
error(
|
|
||||||
"The license $license is currently not covered by policy rules. " +
|
|
||||||
"The license was ${licenseSource.name.lowercase()} in package " +
|
|
||||||
"${pkg.metadata.id.toCoordinates()}.",
|
|
||||||
howToFixDefault()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RuleSet.unmappedDeclaredLicenseRule() = packageRule("UNMAPPED_DECLARED_LICENSE") {
|
|
||||||
require {
|
|
||||||
-isExcluded()
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedLicenseInfo.licenseInfo.declaredLicenseInfo.processed.unmapped.forEach { unmappedLicense ->
|
|
||||||
warning(
|
|
||||||
"The declared license '$unmappedLicense' could not be mapped to a valid license or parsed as an SPDX " +
|
|
||||||
"expression. The license was found in package ${pkg.metadata.id.toCoordinates()}.",
|
|
||||||
howToFixDefault()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val ruleSet = ruleSet(ortResult, licenseInfoResolver, resolutionProvider) {
|
|
||||||
unhandledLicenseRule()
|
|
||||||
unmappedDeclaredLicenseRule()
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleViolations += ruleSet.violations
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
id: "SpdxDocumentFile:The Elixir Team:eex:"
|
|
||||||
path_excludes:
|
|
||||||
- pattern: "lib/eex/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
license_finding_curations:
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/eex/test/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
id: "SpdxDocumentFile:The Elixir Team:elixir:"
|
|
||||||
path_excludes:
|
|
||||||
- pattern: "lib/elixir/pages/**/*"
|
|
||||||
reason: "DOCUMENTATION_OF"
|
|
||||||
comment: "Documentation"
|
|
||||||
- pattern: "lib/elixir/scripts/**/*"
|
|
||||||
reason: "BUILD_TOOL_OF"
|
|
||||||
comment: "Build Tool"
|
|
||||||
- pattern: "lib/elixir/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
license_finding_curations:
|
|
||||||
# Logos
|
|
||||||
- path: "lib/elixir/pages/images/logo.png"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply Trademark Policy to Elixir Logo"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
|
||||||
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply Trademark Policy to Elixir Logo"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
|
||||||
|
|
||||||
# Documentation Images
|
|
||||||
- path: "lib/elixir/pages/images/**/*.png"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to all images"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
|
|
||||||
# Unicode
|
|
||||||
- path: "lib/elixir/unicode/*.txt"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to unicode files"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "LicenseRef-scancode-unicode"
|
|
||||||
|
|
||||||
# Wrongly Identified
|
|
||||||
- path: "lib/elixir/pages/references/library-guidelines.md"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: |
|
|
||||||
The guide mentions multiple licenses for users to choose from.
|
|
||||||
It however is not licensed itself by the mentioned licenses.
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
|
||||||
reason: "INCORRECT"
|
|
||||||
comment: "Ignored by ScanCode"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
id: "SpdxDocumentFile:The Elixir Team:exunit:"
|
|
||||||
path_excludes:
|
|
||||||
- pattern: "lib/ex_unit/examples/**/*"
|
|
||||||
reason: "EXAMPLE_OF"
|
|
||||||
comment: "Example"
|
|
||||||
- pattern: "lib/ex_unit/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
license_finding_curations:
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/ex_unit/test/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
id: "SpdxDocumentFile:The Elixir Team:logger:"
|
|
||||||
path_excludes:
|
|
||||||
- pattern: "lib/logger/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
id: "SpdxDocumentFile:The Elixir Team:mix:"
|
|
||||||
path_excludes:
|
|
||||||
- pattern: "lib/mix/test/**/*"
|
|
||||||
reason: "TEST_OF"
|
|
||||||
comment: "Tests"
|
|
||||||
license_finding_curations:
|
|
||||||
# Test Fixtures
|
|
||||||
- path: "lib/mix/test/fixtures/**/*"
|
|
||||||
reason: "NOT_DETECTED"
|
|
||||||
comment: "Apply default license to test fixtures"
|
|
||||||
detected_license: "NONE"
|
|
||||||
concluded_license: "Apache-2.0"
|
|
||||||
+441
-77
@@ -1,111 +1,475 @@
|
|||||||
<!--
|
# Changelog for Elixir v1.15
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog for Elixir v1.21
|
This release requires Erlang/OTP 24 and later.
|
||||||
|
|
||||||
## v1.21.0-dev
|
Elixir v1.15 is a smaller release with focused improvements
|
||||||
|
on compilation and boot times. This release also completes
|
||||||
|
our integration process with Erlang/OTP logger, bringing new
|
||||||
|
features such as log rotation and compaction out of the box.
|
||||||
|
|
||||||
### 1. Enhancements
|
You will also find additional convenience functions in `Code`,
|
||||||
|
`Map`, `Keyword`, all Calendar modules, and others.
|
||||||
|
|
||||||
#### EEx
|
## Compile and boot-time improvements
|
||||||
|
|
||||||
* [EEx] Support splitting middle expressions across EEx clauses
|
The last several releases brought improvements to compilation
|
||||||
|
time and this version is no different. In particular, Elixir
|
||||||
|
now caches and prunes load paths before compilation, ensuring your
|
||||||
|
project (and dependencies!) compile faster and in an environment
|
||||||
|
closer to production.
|
||||||
|
|
||||||
|
In a nutshell the Erlang VM loads modules from code paths. Each
|
||||||
|
application that ships with Erlang and Elixir plus each dependency
|
||||||
|
become an entry in your code path. The larger the code path, the
|
||||||
|
more work Erlang has to do in order to find a module.
|
||||||
|
|
||||||
|
In previous versions, Mix would only add entries to the load paths.
|
||||||
|
Therefore, if you compiled 20 dependencies and you went to compile
|
||||||
|
the 21st, the code path would have 21 entries (plus all Erlang and
|
||||||
|
Elixir apps). This allowed modules from unrelated dependencies to
|
||||||
|
be seen and made compilation slower the more dependencies you had.
|
||||||
|
With this release, we will now prune the code paths to only the ones
|
||||||
|
listed as dependencies, bringing the behaviour closer to `mix release`.
|
||||||
|
|
||||||
|
Furthermore, Erlang/OTP 26 allows us to start applications
|
||||||
|
concurrently and cache the code path lookups, decreasing the cost of
|
||||||
|
booting applications. The combination of Elixir v1.15 and Erlang/OTP 26
|
||||||
|
should reduce the boot time of applications, such as when starting
|
||||||
|
`iex -S mix` or running a single test with `mix test`, from 5% to 30%.
|
||||||
|
|
||||||
|
The compiler is also smarter in several ways: `@behaviour` declarations
|
||||||
|
no longer add compile-time dependencies and aliases in patterns and
|
||||||
|
guards add no dependency whatsoever, as no dispatching happens. Furthermore,
|
||||||
|
Mix now tracks the digests of `@external_resource` files, reducing the
|
||||||
|
amount of recompilation when swapping branches. Finally, dependencies
|
||||||
|
are automatically recompiled when their compile-time configuration changes.
|
||||||
|
|
||||||
|
### Potential incompatibilities
|
||||||
|
|
||||||
|
Due to the code path pruning, if you have an application or dependency
|
||||||
|
that does not specify its dependencies on Erlang and Elixir application,
|
||||||
|
it may no longer compile successfully in Elixir v1.15. You can temporarily
|
||||||
|
disable code path pruning by setting `prune_code_paths: false` in your
|
||||||
|
`mix.exs`, although doing so may lead to runtime bugs that are only
|
||||||
|
manifested inside a `mix release`.
|
||||||
|
|
||||||
|
## Compiler warnings and errors
|
||||||
|
|
||||||
|
The Elixir compiler can now emit many errors for a single file, making
|
||||||
|
sure more feedback is reported to developers before compilation is aborted.
|
||||||
|
|
||||||
|
In Elixir v1.14, an undefined function would be reported as:
|
||||||
|
|
||||||
|
** (CompileError) undefined function foo/0 (there is no such import)
|
||||||
|
my_file.exs:1
|
||||||
|
|
||||||
|
In Elixir v1.15, the new reports will look like:
|
||||||
|
|
||||||
|
error: undefined function foo/0 (there is no such import)
|
||||||
|
my_file.exs:1
|
||||||
|
|
||||||
|
** (CompileError) my_file.exs: cannot compile file (errors have been logged)
|
||||||
|
|
||||||
|
A new function, called `Code.with_diagnostics/2`, has been added so this
|
||||||
|
information can be leveraged by editors, allowing them to point to several
|
||||||
|
errors at once.
|
||||||
|
|
||||||
|
### Potential incompatibilities
|
||||||
|
|
||||||
|
As part of this effort, the behaviour where undefined variables were
|
||||||
|
transformed into nullary function calls, often leading to confusing error
|
||||||
|
reports, has been disabled during project compilation. You can invoke
|
||||||
|
`Code.compiler_options(on_undefined_variable: :warn)` at the top of
|
||||||
|
your `mix.exs` to bring the old behaviour back.
|
||||||
|
|
||||||
|
## Integration with Erlang/OTP logger
|
||||||
|
|
||||||
|
This release provides additional features such as global logger
|
||||||
|
metadata and file logging (with rotation and compaction) out-of-the-box!
|
||||||
|
|
||||||
|
This release also soft-deprecates Elixir's Logger Backends in
|
||||||
|
favor of Erlang's Logger handlers. Elixir will automatically
|
||||||
|
convert your `:console` backend configuration into the new
|
||||||
|
configuration. Previously, you would set:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :logger, :console,
|
||||||
|
level: :error,
|
||||||
|
format: "$time $message $metadata"
|
||||||
|
```
|
||||||
|
|
||||||
|
Which is now translated to the equivalent:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :logger, :default_handler,
|
||||||
|
level: :error
|
||||||
|
|
||||||
|
config :logger, :default_formatter,
|
||||||
|
format: "$time $message $metadata"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use `Logger.Backends.Console` with a custom device or other
|
||||||
|
backends, they are still fully supported and functional. If you
|
||||||
|
implement your own backends, you want to consider migrating to
|
||||||
|
[`:logger_backends`](https://github.com/elixir-lang/logger_backends)
|
||||||
|
in the long term.
|
||||||
|
|
||||||
|
See the new `Logger` documentation for more information on the
|
||||||
|
new features and on compatibility.
|
||||||
|
|
||||||
|
## v1.15.8 (2024-05-21)
|
||||||
|
|
||||||
|
### 1. Bug fixes
|
||||||
|
|
||||||
#### Elixir
|
#### Elixir
|
||||||
|
|
||||||
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
|
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
|
||||||
* [Code] Add support for the `:erlc_options` compiler option
|
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
|
||||||
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
|
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
|
||||||
* [Kernel] Improve performance of type constructors and complex intersections
|
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
|
||||||
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
|
|
||||||
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
|
|
||||||
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
|
|
||||||
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
|
|
||||||
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
|
|
||||||
* [Path] Add `Path.safe_join/2`
|
|
||||||
* [Registry] Optimize exact key matching in lookups
|
|
||||||
* [String] Optimize `String.bag_distance/2`
|
|
||||||
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
|
|
||||||
* [URI] Optimize percent-decoding and `URI.to_string/1`
|
|
||||||
|
|
||||||
#### ExUnit
|
#### Logger
|
||||||
|
|
||||||
* [ExUnit.Assertions] Add `trace/3` helper
|
* [Logger] Ensure translators are persisted across logger restarts
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix compile] Ensure compile paths are accessible during compilation
|
||||||
|
|
||||||
|
## v1.15.7 (2023-10-14)
|
||||||
|
|
||||||
|
### 1. Enhancements
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [Elixir] Allow code evaluation across Elixir versions
|
||||||
|
|
||||||
|
### 2. Bug fixes
|
||||||
|
|
||||||
|
#### EEx
|
||||||
|
|
||||||
|
* [EEx] Do not emit duplicate warnings from tokenizer
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix format] Correctly match file to subdirectory in `Mix.Tasks.Format.formatter_for_file/2`
|
||||||
|
|
||||||
|
## v1.15.6 (2023-09-20)
|
||||||
|
|
||||||
|
This release also includes fixes to the Windows installer.
|
||||||
|
|
||||||
|
### 1. Bug fixes
|
||||||
|
|
||||||
|
#### EEx
|
||||||
|
|
||||||
|
* [EEx] Do not crash when printing tokenizer warnings
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [Code] Fix formatter for nested `*` in bitstrings
|
||||||
|
* [Code] Improve feedback when an invalid block is given `Code.quoted_to_algebra/2`
|
||||||
|
* [Kernel] Trace functions before they are inlined
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix compile] Ensure `:extra_applications` declare in umbrella projects are loaded
|
||||||
|
* [mix deps.get] Do not check for invalid applications before deps.get
|
||||||
|
* [mix deps.update] Do not check for invalid applications before deps.update
|
||||||
|
* [mix format] Load plugins when invoking the formatter from an IDE
|
||||||
|
|
||||||
|
## v1.15.5 (2023-08-28)
|
||||||
|
|
||||||
|
### 1. Enhancements
|
||||||
|
|
||||||
|
#### IEx
|
||||||
|
|
||||||
|
* [IEx.Autocomplete] Speed up loading of struct suggestions
|
||||||
|
|
||||||
### 2. Bug fixes
|
### 2. Bug fixes
|
||||||
|
|
||||||
#### Elixir
|
#### Elixir
|
||||||
|
|
||||||
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y`
|
* [Code.Fragment] Fix `Code.Fragment.surround_context/2` for aliases and submodules of non-aliases
|
||||||
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3`
|
* [Kernel] Ensure stacktrace is included when necessary when rescuing multiple exceptions in the same branch
|
||||||
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision
|
* [Kernel] Fix index in error message for unused optional arguments
|
||||||
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats
|
|
||||||
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments
|
|
||||||
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
|
|
||||||
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
|
|
||||||
* [Date.Range] Fix slicing date ranges with stepped ranges
|
|
||||||
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
|
|
||||||
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
|
|
||||||
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
|
|
||||||
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
|
|
||||||
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
|
|
||||||
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
|
|
||||||
* [Kernel] Fix inferred types for functions with non-returning clauses
|
|
||||||
* [Kernel] Fix map field type inference in the presence of empty map types
|
|
||||||
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
|
|
||||||
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
|
|
||||||
* [Kernel] Improve the error message for non-atom struct keys
|
|
||||||
* [Kernel] Raise when `|` is used in guards
|
|
||||||
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
|
|
||||||
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
|
|
||||||
* [Macro] Properly escape C1 control characters and Unicode noncharacters
|
|
||||||
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
|
|
||||||
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
|
|
||||||
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
|
|
||||||
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
|
|
||||||
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
|
|
||||||
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
|
|
||||||
|
|
||||||
#### ExUnit
|
#### ExUnit
|
||||||
|
|
||||||
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
|
* [ExUnit.Diff] Fix scenario where diff would not show up due to a timed-out loop
|
||||||
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
|
|
||||||
|
|
||||||
#### IEx
|
#### IEx
|
||||||
|
|
||||||
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys
|
* [IEx] Force group leader to run as a binary and unicode in IEx
|
||||||
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
|
|
||||||
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
|
|
||||||
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
|
|
||||||
|
|
||||||
#### Mix
|
#### Mix
|
||||||
|
|
||||||
* [Mix] Prevent synchronization lock files from being overwritten with empty contents
|
* [mix compile] Do not assume `blake` is always available
|
||||||
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
|
* [mix format] Load and compile plugins if specified in subdirectories
|
||||||
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
|
|
||||||
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
|
|
||||||
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
|
|
||||||
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
|
|
||||||
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
|
|
||||||
* [mix new] Avoid trailing whitespace in generated files
|
|
||||||
|
|
||||||
### 3. Hard deprecations
|
## v1.15.4 (2023-07-18)
|
||||||
|
|
||||||
|
### 1. Bug fixes
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix archive.build] Disable protocol consolidation when building archives on archive.install
|
||||||
|
* [mix compile] Track removed files per local dependency (this addresses a bug where files depending on modules from path dependencies always recompiled)
|
||||||
|
* [mix release] Do not strip relevant chunks from Erlang/OTP 26
|
||||||
|
|
||||||
|
## v1.15.3 (2023-07-15)
|
||||||
|
|
||||||
|
### 1. Enhancements
|
||||||
|
|
||||||
#### Elixir
|
#### Elixir
|
||||||
|
|
||||||
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
|
* [Kernel] Improve stacktraces when executing unnested Elixir code in a file
|
||||||
|
|
||||||
### 4. Soft deprecations
|
#### Mix
|
||||||
|
|
||||||
|
* [Mix] Allow to opt-out of starting apps in `Mix.install/2`
|
||||||
|
|
||||||
|
### 2. Bug fixes
|
||||||
|
|
||||||
#### Elixir
|
#### Elixir
|
||||||
|
|
||||||
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
|
* [Code] Ensure `with_diagnostics` propagate warnings from inner Erlang passes
|
||||||
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
|
|
||||||
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
|
|
||||||
|
|
||||||
## v1.20
|
#### IEx
|
||||||
|
|
||||||
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
|
* [IEx] Fix `--remsh` on Erlang/OTP 25 and earlier
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix compile.elixir] Ensure `__mix_recompile__?` callbacks are properly invoked
|
||||||
|
|
||||||
|
## v1.15.2 (2023-07-01)
|
||||||
|
|
||||||
|
### 1. Bug fixes
|
||||||
|
|
||||||
|
#### IEx
|
||||||
|
|
||||||
|
* [IEx] Fix CLI being unable to boot on Windows
|
||||||
|
|
||||||
|
## v1.15.1 (2023-06-30)
|
||||||
|
|
||||||
|
### 1. Enhancements
|
||||||
|
|
||||||
|
* [Code] `Code.string_to_quoted/2` honors `:static_atoms_encoder` for multi-letter sigils
|
||||||
|
|
||||||
|
### 2. Bug fixes
|
||||||
|
|
||||||
|
#### ExUnit
|
||||||
|
|
||||||
|
* [ExUnit.CaptureLog] Fix race condition on concurrent `capture_log`
|
||||||
|
* [ExUnit.CaptureLog] Respect options passed to nested `capture_log` calls
|
||||||
|
* [ExUnit.Doctest] Properly compile doctests without results terminated by fences
|
||||||
|
* [ExUnit.Doctest] Allow variables defined in doctests to be used in expectation
|
||||||
|
|
||||||
|
#### IEx
|
||||||
|
|
||||||
|
* [IEx] Ensure `pry` works on Erlang/OTP 25 and earlier while IEx is booting
|
||||||
|
* [IEx] `Code.Fragment.surround_context` considers surround context around spaces and parens
|
||||||
|
|
||||||
|
#### Logger
|
||||||
|
|
||||||
|
* [Logger] Do not assume Logger has been loaded at compile-time
|
||||||
|
* [Logger.Formatter] Properly handle `:function` as metadata
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix compile] Ensure the current project is available on the code path after its Elixir sources are compiled
|
||||||
|
* [mix compile] Guarantee yecc/leex are available when emitting warnings from previous runs
|
||||||
|
* [mix compile] Fix bug where an external resource was deleted after its
|
||||||
|
mtime was successfully retrieved
|
||||||
|
* [mix compile] Track removed modules and exports across local deps
|
||||||
|
* [mix deps] Fix an issue where dependencies could not be started in an umbrella projects
|
||||||
|
* [mix release] Properly handle optional dependencies when there is a conflict in the application start mode
|
||||||
|
* [mix release] Remove `--werl` from release scripts on Erlang/OTP 26
|
||||||
|
|
||||||
|
## v1.15.0 (2023-06-19)
|
||||||
|
|
||||||
|
### 1. Enhancements
|
||||||
|
|
||||||
|
#### EEx
|
||||||
|
|
||||||
|
* [EEx] Include source code snippets in syntax errors
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [Calendar] Add support for epoch time (`%s`) to `Calendar.strftime/2`
|
||||||
|
* [Code] `Code.format_string!/2` now converts `'charlists'` into `~c"charlists"` by default
|
||||||
|
* [Code] Add `:on_undefined_variable` to the compiler options to preserve the warning behaviour which was deprecated back in Elixir v1.4
|
||||||
|
* [Code] Add `Code.loaded?/1` and `Code.ensure_all_loaded(!)/1`
|
||||||
|
* [Code] Add `Code.prepend_paths/1`, `Code.append_paths/1`, and `Code.delete_paths/1`
|
||||||
|
* [Code] Add `Code.with_diagnostics/2` to return diagnostics when compiling and evaluating code
|
||||||
|
* [Code.Fragment] Support nested expressions in `Code.Fragment.cursor_context/1`
|
||||||
|
* [Code.Fragment] Keep operators and no paren calls in `Code.Fragment.container_cursor_to_quoted/1`
|
||||||
|
* [Date] Add `Date.before?/2` and `Date.after?/2`
|
||||||
|
* [DateTime] Add `DateTime.before?/2` and `DateTime.after?/2`
|
||||||
|
* [DateTime] Support precision in `DateTime.utc_now/2`
|
||||||
|
* [File] Support distributed `File.Stream`
|
||||||
|
* [Inspect] `Inspect` now renders `'charlists'` as `~c"charlists"` by default
|
||||||
|
* [Kernel] Break down `case` and `cond` inside `dbg/2`
|
||||||
|
* [Kernel] Add `t:nonempty_binary/0` and `t:nonempty_bitstring/0`
|
||||||
|
* [Kernel] Treat `@behaviour`s as runtime dependencies
|
||||||
|
* [Kernel] Do not add runtime dependencies for alias references in patterns and guards
|
||||||
|
* [Kernel] Warn for nested calls without parens inside keywords
|
||||||
|
* [Kernel] Support for multi-letter uppercase sigils
|
||||||
|
* [Kernel] Introduce mechanism to collect several errors in a module. Previously, as soon as there was a compilation error, compilation would fail. Now the compiler became a bit smarter and will report multiple errors whenever possible as multiple `error: ...` messages, similar to `warning: ...`
|
||||||
|
* [Kernel] Raise instead of warning on undefined variables. Previously, an undefined variable would attempt to invoke a function of the same name, which led to confusing error messages, especially to newcomers. To enable the previous behaviour, invoke `Code.compiler_options(on_undefined_variable: :warn)` at the top of your `mix.exs`
|
||||||
|
* [Kernel.CLI] Support `--sname undefined`/`--name undefined` so a name is automatically generated
|
||||||
|
* [Keyword] Add `Keyword.split_with/2`
|
||||||
|
* [Macro] Improve error message when piping into an expression ending in bracket-based access
|
||||||
|
* [Macro.Env] Add `Macro.Env.lookup_alias_as/2`
|
||||||
|
* [Map] Add `Map.split_with/2`
|
||||||
|
* [Map] Add `Map.intersect/2` and `Map.intersect/3`
|
||||||
|
* [MapSet] Add `MapSet.split_with/2`
|
||||||
|
* [MapSet] Optimize most functions
|
||||||
|
* [NaiveDateTime] Add `NaiveDateTime.beginning_of_day/1` and `NaiveDateTime.end_of_day/1`
|
||||||
|
* [NaiveDateTime] Add `NaiveDateTime.before?/2` and `NaiveDateTime.after?/2`
|
||||||
|
* [NaiveDateTime] Support precision in `NaiveDateTime.utc_now/2`
|
||||||
|
* [Module] Mark functions as generated in "Docs" chunk
|
||||||
|
* [Module] Add `Module.get_last_attribute/3`
|
||||||
|
* [OptionParser] Support `:return_separator` option
|
||||||
|
* [Process] Add `Process.alias/0,1` and `Process.unalias/1`
|
||||||
|
* [Range] Add `Range.split/2`
|
||||||
|
* [String] Update Unicode to version 15.0.0
|
||||||
|
* [String] Add `:fast_ascii` mode to `String.valid?/2`
|
||||||
|
* [Supervisor] Add support for automatic shutdown in `Supervisor`
|
||||||
|
* [System] Support `:lines` in `System.cmd/3` to capture output line by line
|
||||||
|
* [Task] Remove head of line blocking on `Task.yield_many/2`
|
||||||
|
* [Task] Enable selective receive optimizations in Erlang/OTP 26+
|
||||||
|
* [Task] Reduce tasks footprint by avoiding unecessary work during spawning
|
||||||
|
* [Task.Supervisor] Do not copy args on temporary `Task.Supervisor.start_child/2`
|
||||||
|
* [Time] Add `Time.before?/2` and `Time.after?/2`
|
||||||
|
* [URI] Add `URI.append_path/2`
|
||||||
|
|
||||||
|
#### ExUnit
|
||||||
|
|
||||||
|
* [ExUnit] Add more color configuration to ExUnit CLI formatter
|
||||||
|
* [ExUnit.Callbacks] Accept `{module, function}` tuples in ExUnit `setup` callbacks
|
||||||
|
* [ExUnit.Case] Add `ExUnit.Case.get_last_registered_test/1`
|
||||||
|
* [ExUnit.Doctest] Add `ExUnit.DocTest.doctest_file/2`
|
||||||
|
* [ExUnit.Doctest] Include `doctest_data` in doctest tags
|
||||||
|
* [ExUnit.Formatter] When comparing two anonymous functions, defined at the same place but capturing a different environment, we will now also diff the environments
|
||||||
|
|
||||||
|
#### IEx
|
||||||
|
|
||||||
|
* [IEx] Make pry opt-in on dbg with `--dbg pry`
|
||||||
|
* [IEX] Support `IEX_HOME`
|
||||||
|
* [IEx.Autocomplete] Only provide aliases when autocompleting `alias`, `import`, and `require`
|
||||||
|
* [IEx.Autocomplete] Provide field completion on map and struct updates
|
||||||
|
* [IEx.Helpers] Add `runtime_info(:allocators)`
|
||||||
|
* [IEx.Info] Implement protocol for `Range`, `DateTime`, and `Regex`
|
||||||
|
|
||||||
|
#### Logger
|
||||||
|
|
||||||
|
* [Logger] Add `Logger.add_handlers/1` and `Logger.default_formatter/1`
|
||||||
|
* [Logger] Introduce `default_formatter` and `default_handler` configuration for Logger which configures Erlang/OTP logger
|
||||||
|
* [Logger] Add `:always_evaluate_messages` configuration to Logger
|
||||||
|
* [Logger.Formatter] Implement the Erlang Logger formatter API
|
||||||
|
* [Logger.Formatter] Add support for ports in Logger metadata
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix app.start] Allow applications to be started concurrently via the `:start_concurrently` configuration
|
||||||
|
* [mix compile] Set `--all-warnings` by default
|
||||||
|
* [mix compile] Reduce the amount of filesystem lookups for path dependencies by storing timestamps in manifests
|
||||||
|
* [mix compile] Track digests of `@external_resources`
|
||||||
|
* [mix compile.app] Write `optional_applications` to `.app` file
|
||||||
|
* [mix compile.elixir] Add `--purge-consolidation-path-if-stale` which will purge the given consolidation path if compilation is required
|
||||||
|
* [mix deps.compile] Automatically recompile dependencies if their compile env changes
|
||||||
|
* [mix deps.get] Automatically install Hex and Rebar on `mix deps.get`/`mix deps.update`
|
||||||
|
* [mix deps.get] Support `--check-locked` which raises if changes to the lockfile are required
|
||||||
|
* [mix eval] Allow passing additional arguments
|
||||||
|
* [mix format] Support `--no-exit` option
|
||||||
|
* [mix format] Allow multiple formatters per file extension and sigil
|
||||||
|
* [mix format] Show diffs whenever `--check-formatted` fails
|
||||||
|
* [mix format] Allow the formatting root to be configured
|
||||||
|
* [mix loadpaths] Cache deps and archive loadpaths in Erlang/OTP 26
|
||||||
|
* [mix profile.fprof] Support `--trace-to-file` to improve performance when working with large outputs
|
||||||
|
* [mix release] Allow passing additional arguments to the `eval` command
|
||||||
|
* [mix xref graph] Support `--output` flag
|
||||||
|
* [Mix.Project] Support `def cli` to unify all CLI defaults in a single place
|
||||||
|
* [Mix.Project] Add `Mix.Project.deps_tree/1`
|
||||||
|
|
||||||
|
### 2. Bug fixes
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [Code.Formatter] Fix a scenario where a keyword followed by parenthesis could go above the maximum line length
|
||||||
|
* [Code.Formatter] Remove unnecessary parens in nullary type funs
|
||||||
|
* [Exception] Fix operator precedence when printing guards in `Exception.blame/3`
|
||||||
|
* [File] Do not raise if there are file system race conditions in `File.cp/2`
|
||||||
|
* [File] Do not raise when deleting write-only empty directories on `File.rm_rf/1`
|
||||||
|
* [Kernel] Expand macros on the left side of -> in `try/rescue`
|
||||||
|
* [Kernel] Raise on misplaced `...` inside typespecs
|
||||||
|
* [Kernel] Do not import `behaviour_info` and `module_info` functions from Erlang modules
|
||||||
|
* [Kernel] Raise when macros are given to dialyzer
|
||||||
|
* [Kernel.ParallelCompiler] Make sure compiler doesn't crash when there are stray messages in the inbox
|
||||||
|
* [Kernel.ParallelCompiler] Track compile and runtime warnings separately
|
||||||
|
* [Module] Ensure that `Module.get_attribute/3` returns `nil` and not the given default value when an attribute has been explicitly set as `nil`
|
||||||
|
* [System] Fix race condition when a script would terminate before `System.stop/1` executes
|
||||||
|
* [Task] Do not double log Task failure reports
|
||||||
|
* [URI] Make sure `URI.merge/2` works accordingly with relative paths
|
||||||
|
|
||||||
|
#### ExUnit
|
||||||
|
|
||||||
|
* [ExUnit] Fix crash when `@tag capture_log: true` was set to true and the Logger application was shut down in the middle of the test
|
||||||
|
* [ExUnit] Do not merge context as tags inside the runner to reduce memory usage when emitting events to formatters
|
||||||
|
* [ExUnit] Mark test cases as invalid when an exit occurs during `setup_all`
|
||||||
|
* [ExUnit] Do not expand or collect vars from quote in ExUnit assertions
|
||||||
|
* [ExUnit.DocTest] Ensure proper line is returned when failing to parse doctest results
|
||||||
|
* [ExUnit.Doctest] Fix line information when a doctest with multiple assertions fails
|
||||||
|
|
||||||
|
#### IEx
|
||||||
|
|
||||||
|
* [IEx] Do not spawn a process to read IO. This fixes a bug where multiline paste stopped working
|
||||||
|
whenever the input reader was killed
|
||||||
|
* [IEx] Do not perform completion for prompts triggered during code evaluation
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [mix compile] Include `cwd` in compiler cache key
|
||||||
|
* [mix release] Fix Windows service when invoking `erlsrv.exe` in path with spaces
|
||||||
|
* [mix xref] Raise early if `mix xref` is used at the umbrella root
|
||||||
|
|
||||||
|
### 3. Soft deprecations (no warnings emitted)
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [File] `File.cp/3` and `File.cp_r/3` with a function as third argument
|
||||||
|
is deprecated in favor of a keyword list
|
||||||
|
* [Kernel] Require pin variable when accessing variable inside binary size in match
|
||||||
|
* [Kernel.ParallelCompiler] Require the `:return_diagnostics` option to be
|
||||||
|
set to true when compiling or requiring code
|
||||||
|
|
||||||
|
#### Logger
|
||||||
|
|
||||||
|
* [Logger] `add_backend/2`, `remove_backend/2`, and `configure_backend/2` have been deprecated
|
||||||
|
in favor of the new `:logger_backends` dependency
|
||||||
|
* [Logger] The `:console` configuration has been deprecated in favor of `:default_formatter`
|
||||||
|
* [Logger] The `:backends` configuration has been deprecated in favor of `Logger.add_handlers/1`
|
||||||
|
|
||||||
|
#### Mix
|
||||||
|
|
||||||
|
* [Mix.Project] `:preferred_cli_env` is deprecated in favor of `:preferred_envs` in `def cli`
|
||||||
|
* [Mix.Project] `:preferred_cli_target` is deprecated in favor of `:preferred_targets` in `def cli`
|
||||||
|
* [mix local] The environment variable `HEX_MIRROR` is deprecated in favor of `HEX_BUILDS_URL`
|
||||||
|
|
||||||
|
### 4. Hard deprecations
|
||||||
|
|
||||||
|
#### Elixir
|
||||||
|
|
||||||
|
* [Calendar] `Calendar.ISO.day_of_week/3` is deprecated in favor of `Calendar.ISO.day_of_week/4`
|
||||||
|
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
|
||||||
|
* [Kernel] Deprecate `...` as a valid function call identifier
|
||||||
|
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
|
||||||
|
|
||||||
|
#### Logger
|
||||||
|
|
||||||
|
* [Logger] `Logger.warn/2` is deprecated in favor of `Logger.warning/2`
|
||||||
|
|
||||||
|
## v1.14
|
||||||
|
|
||||||
|
The CHANGELOG for v1.14 releases can be found [in the v1.14 branch](https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md).
|
||||||
|
|||||||
+5
-11
@@ -1,12 +1,6 @@
|
|||||||
<!--
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
Contact: <elixir-lang-conduct@googlegroups.com>
|
Contact: elixir-lang-conduct@googlegroups.com
|
||||||
|
|
||||||
## Why have a Code of Conduct?
|
## Why have a Code of Conduct?
|
||||||
|
|
||||||
@@ -51,15 +45,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
|
|||||||
|
|
||||||
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
||||||
|
|
||||||
* The [official GitHub projects][1] and code reviews.
|
* The [official GitHub projects][1] and code reviews.
|
||||||
* The official elixir-lang mailing lists.
|
* The official elixir-lang mailing lists.
|
||||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||||
|
|
||||||
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
||||||
|
|
||||||
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||||
|
|
||||||
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
||||||
|
|
||||||
|
|||||||
-244
@@ -1,244 +0,0 @@
|
|||||||
<!--
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Contributing to Elixir
|
|
||||||
|
|
||||||
We invite contributions to Elixir. To contribute, there are a few
|
|
||||||
things you need to know about the code. First, Elixir code is divided
|
|
||||||
by each application inside the `lib` folder:
|
|
||||||
|
|
||||||
* `elixir` - Elixir's kernel and standard library
|
|
||||||
|
|
||||||
* `eex` - EEx is the template engine that allows you to embed Elixir
|
|
||||||
|
|
||||||
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
|
|
||||||
|
|
||||||
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
|
|
||||||
|
|
||||||
* `logger` - Logger is the built-in logger
|
|
||||||
|
|
||||||
* `mix` - Mix is Elixir's build tool
|
|
||||||
|
|
||||||
You can run all tests in the root directory with `make test`. You can
|
|
||||||
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
|
|
||||||
`make test_ex_unit`. If you just changed something in Elixir's standard
|
|
||||||
library, you can run only that portion through `make test_stdlib`.
|
|
||||||
|
|
||||||
If you are only changing one file, you can choose to compile and run tests
|
|
||||||
for that specific file for faster development cycles. For example, if you
|
|
||||||
are changing the String module, you can compile it and run its tests as:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
|
||||||
bin/elixir lib/elixir/test/elixir/string_test.exs
|
|
||||||
```
|
|
||||||
|
|
||||||
Some test files need their `test_helper.exs` to be explicitly required
|
|
||||||
before, such as:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also use the `LINE` env var to run a single test:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
|
|
||||||
````
|
|
||||||
|
|
||||||
To recompile all (including Erlang modules):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make compile
|
|
||||||
```
|
|
||||||
|
|
||||||
After your changes are done, run `make format` to guarantee
|
|
||||||
all files are properly formatted, then run the full suite with
|
|
||||||
`make test`.
|
|
||||||
|
|
||||||
If your contribution fails during the bootstrapping of the language,
|
|
||||||
you can rebuild the language from scratch with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make clean_elixir compile
|
|
||||||
```
|
|
||||||
|
|
||||||
Similarly, if you can not get Elixir to compile or the tests to pass after
|
|
||||||
updating an existing checkout, run `make clean compile`. You can check
|
|
||||||
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
|
||||||
More tasks can be found by reading the [Makefile](Makefile).
|
|
||||||
|
|
||||||
## Sending a pull request
|
|
||||||
|
|
||||||
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
|
|
||||||
and must include tests and other relevant proof of work:
|
|
||||||
|
|
||||||
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
|
|
||||||
your change and *passes* afterward. This makes it easier to confirm that the
|
|
||||||
fix addresses the underlying issue and helps prevent regressions in the future.
|
|
||||||
|
|
||||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
|
||||||
major changes to existing functionality, please add assocaited tests. Aim to
|
|
||||||
have the best code coverage possible.
|
|
||||||
|
|
||||||
* **Performance improvements:** For performance improvements, please include the
|
|
||||||
benchmark script, with inputs and results, in the pull request description.
|
|
||||||
We recommend using [benchee](https://github.com/bencheeorg/benchee).
|
|
||||||
|
|
||||||
Here are some pull requests we have received in the past you can use as reference:
|
|
||||||
|
|
||||||
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
|
|
||||||
|
|
||||||
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
|
|
||||||
|
|
||||||
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
|
|
||||||
|
|
||||||
## Reviewing changes
|
|
||||||
|
|
||||||
Once a pull request is sent, the Elixir team will review your changes.
|
|
||||||
If changes are necessary, the team will leave appropriate comments requesting
|
|
||||||
changes to the code. Unfortunately, we cannot guarantee a pull request will
|
|
||||||
be merged, even when modifications are requested, as the Elixir team will
|
|
||||||
re-evaluate the contribution as it changes.
|
|
||||||
|
|
||||||
When the review finishes, your pull request will be squashed and merged
|
|
||||||
into the repository. If you have carefully organized your commits and
|
|
||||||
believe they should be merged without squashing, please mention it in
|
|
||||||
a comment.
|
|
||||||
|
|
||||||
## Building documentation
|
|
||||||
|
|
||||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
|
||||||
is cloned and compiled alongside Elixir.
|
|
||||||
|
|
||||||
After cloning and compiling Elixir, run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
elixir_dir=$(pwd)
|
|
||||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
|
||||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
|
||||||
|
|
||||||
# Now we will go back to Elixir's root directory,
|
|
||||||
cd "${elixir_dir}"
|
|
||||||
|
|
||||||
# and generate HTML and EPUB documents:
|
|
||||||
make docs
|
|
||||||
```
|
|
||||||
|
|
||||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
|
||||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
|
||||||
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
|
|
||||||
|
|
||||||
## Licensing and Compliance Requirements
|
|
||||||
|
|
||||||
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
|
||||||
guidelines on licensing and compliance. Below is a summary of the key points
|
|
||||||
affecting **all external contributors**:
|
|
||||||
|
|
||||||
* Accepted Licenses: Any code contributed must be licensed under the
|
|
||||||
`Apache-2.0` license.
|
|
||||||
|
|
||||||
* SPDX License Headers: With the exception of approved test fixture files,
|
|
||||||
all new or modified files in a pull request must include correct SPDX
|
|
||||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
|
||||||
instance, please use:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
```
|
|
||||||
|
|
||||||
* No Executable Binaries: Contributions must **not** include any executable
|
|
||||||
binary files. If you require an exception (for example, certain test artifacts),
|
|
||||||
please see the policy on how to request approval and document exceptions.
|
|
||||||
|
|
||||||
* Preserving Copyright and License Info: If you copy code from elsewhere,
|
|
||||||
ensure that **all original copyright and license notices remain intact**. If
|
|
||||||
they are missing or incomplete, you must add them.
|
|
||||||
|
|
||||||
* Failure to Comply: Pull requests that do not meet these licensing and
|
|
||||||
compliance standards will be rejected or require modifications before merging.
|
|
||||||
|
|
||||||
* Developer Certificate of Origin: All contributions are subject to the
|
|
||||||
Developer Certificate of Origin.
|
|
||||||
|
|
||||||
```text
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the
|
|
||||||
best of my knowledge, is covered under an appropriate open
|
|
||||||
source license and I have the right under that license to
|
|
||||||
submit that work with modifications, whether created in whole
|
|
||||||
or in part by me, under the same open source license (unless
|
|
||||||
I am permitted to submit under a different license), as
|
|
||||||
Indicated in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including
|
|
||||||
all personal information I submit with it, including my
|
|
||||||
sign-off) is maintained indefinitely and may be redistributed
|
|
||||||
consistent with this project or the open source license(s)
|
|
||||||
involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
|
||||||
of Origin license.
|
|
||||||
|
|
||||||
## Using AI and coding agents
|
|
||||||
|
|
||||||
While we allow the use of AI on contributions and discussions, please be mindful
|
|
||||||
when doing so. Generally speaking, Elixir maintainers already have access to AI
|
|
||||||
(like many other developers). Therefore, if we need the feedback or help of a
|
|
||||||
coding agent, we can request so ourselves. For this reason, we often find
|
|
||||||
the point of view of the human behind the agent more valuable.
|
|
||||||
|
|
||||||
That said, here are examples of how one might (or might not) use AI and coding
|
|
||||||
agents in Elixir spaces:
|
|
||||||
|
|
||||||
* When it comes to discussions, using AI to help express yourself is welcome,
|
|
||||||
but avoid directly copy and pasting AI generated content. If there is a language
|
|
||||||
barrier, use AI to translate, review, and improve your text, but do not use AI
|
|
||||||
to respond on your behalf.
|
|
||||||
|
|
||||||
* Do not use coding agents to tackle existing issues unless they have the
|
|
||||||
"Contributions Welcome" label.
|
|
||||||
|
|
||||||
* If you request a feature on the mailing list and it is accepted, you may
|
|
||||||
use coding agents to implement it, as long as it follows the AI Contributions
|
|
||||||
guidelines below.
|
|
||||||
|
|
||||||
* When automating AI usage on the Elixir codebase for performance improvements,
|
|
||||||
security fixes, or correctness changes to the compiler or type system, pair it
|
|
||||||
with a separate set of agents whose job is to argue against and try to invalidate
|
|
||||||
any proposed change. And treat their approval as advisory: a human must still
|
|
||||||
validate it before opening issues or pull requests.
|
|
||||||
|
|
||||||
If any code is written by AI, then you must follow the guidelines below.
|
|
||||||
|
|
||||||
### AI contributions
|
|
||||||
|
|
||||||
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
|
|
||||||
Certificate of Origin (DCO). The human submitter is responsible for:
|
|
||||||
|
|
||||||
* Reviewing all AI-generated code
|
|
||||||
* Ensuring compliance with licensing requirements
|
|
||||||
* Adding their own Signed-off-by tag to certify the DCO
|
|
||||||
* Taking full responsibility for the contribution
|
|
||||||
* Disclosing use of AI for comments and code contributions
|
|
||||||
|
|
||||||
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
|
|
||||||
AI in the development process. Contributions should include an Assisted-by tag in the
|
|
||||||
following format:
|
|
||||||
|
|
||||||
Assisted-by: AGENT_NAME:MODEL_VERSION
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
ELIXIR TEAM TRADEMARKS POLICY
|
|
||||||
|
|
||||||
This document outlines the policy for allowed usage of the “Elixir” word and the
|
|
||||||
Elixir logo by other parties.
|
|
||||||
|
|
||||||
“Elixir” and the Elixir logo are registered trademarks of the Elixir Team. The
|
|
||||||
Elixir Team believes in a decentralized approach to growing the community and
|
|
||||||
the ecosystem, independent of the Elixir project and the Elixir Team.
|
|
||||||
|
|
||||||
Anyone can use the Elixir trademarks if that use of the trademark is nominative.
|
|
||||||
The trademarks must not be used to disparage the project and its community, nor
|
|
||||||
be used in any way to imply ownership, endorsement, or association with the
|
|
||||||
Elixir project and the Elixir Team.
|
|
||||||
|
|
||||||
You must not visually combine the Elixir logo with any other images, or change
|
|
||||||
the logo in any way other than ways required by printing restrictions. If you
|
|
||||||
want to create your own visual identity in relation to Elixir, you might use the
|
|
||||||
shape of an unrelated “water drop” as part of your design, as seen in many
|
|
||||||
community projects and initiatives. You must not combine or modify the Elixir
|
|
||||||
logo.
|
|
||||||
|
|
||||||
The Elixir logo is available in our repository in both vertical and horizontal
|
|
||||||
versions.
|
|
||||||
|
|
||||||
Nominative use
|
|
||||||
The “nominative use” (or “nominative fair use”) is a legal doctrine that
|
|
||||||
authorizes everyone (even commercial companies) to use or refer to the trademark
|
|
||||||
of another if:
|
|
||||||
|
|
||||||
The product or service in question must be one not readily identifiable without
|
|
||||||
use of the trademark.
|
|
||||||
|
|
||||||
Only so much of the mark or marks may be used as is reasonably necessary to
|
|
||||||
identify the product or service.
|
|
||||||
|
|
||||||
The organization using the mark must do nothing that would, in conjunction with
|
|
||||||
the mark, suggest sponsorship or endorsement by the trademark holder.
|
|
||||||
|
|
||||||
Our trademarks must be used to refer to the Elixir programming language.
|
|
||||||
|
|
||||||
Examples of permitted use
|
|
||||||
All examples listed next must strictly adhere to the terms outlined in the
|
|
||||||
previous sections:
|
|
||||||
|
|
||||||
Usage of the Elixir logo to say a technology is “powered by Elixir” under
|
|
||||||
nominative use. Linking back to the Elixir website, if possible, is appreciated.
|
|
||||||
|
|
||||||
Usage of the Elixir logo to display it as a supported technology in a service or
|
|
||||||
platform. For instance, you may say “we support Elixir” and use the Elixir logo,
|
|
||||||
but you may not refer to yourself as “the Elixir platform” nor imply any form of
|
|
||||||
endorsement or association with Elixir.
|
|
||||||
|
|
||||||
Usage of the Elixir logo in non-commercial community meetups, in presentations,
|
|
||||||
and in courses when referring to the language and its ecosystem under nominative
|
|
||||||
use.
|
|
||||||
|
|
||||||
Usage of the Elixir logo in non-commercial swag (stickers, t-shirts, mugs, etc)
|
|
||||||
to promote the Elixir programming language. The Elixir marks must be the only
|
|
||||||
marks featured in the product. You need permission to make swag that include
|
|
||||||
Elixir and other third party marks in them.
|
|
||||||
|
|
||||||
Inclusion of the Elixir logo in non-commercial icon sets. Use of the Elixir
|
|
||||||
icons must still adhere to Elixir’s trademark policies.
|
|
||||||
|
|
||||||
Usage of the “Elixir” word in book titles, meetups, conferences, and podcasts.
|
|
||||||
You must not use the word to imply uniqueness or endorsement from the Elixir
|
|
||||||
team. “The Elixir book” and “The Elixir podcast” are not permitted.
|
|
||||||
“Elixir in Action”, “Thinking Elixir”, and “Kraków Elixir User Group” are valid
|
|
||||||
examples already in use today.
|
|
||||||
|
|
||||||
Usage of the “Elixir” word in the names of freely distributed software and
|
|
||||||
hardware products is allowed when referring to use with or suitability for the
|
|
||||||
Elixir programming language, such as wxElixir, Elixirsense, etc. If the product
|
|
||||||
includes the Elixir programming language itself, then you must also respect its
|
|
||||||
license.
|
|
||||||
|
|
||||||
Examples of not permitted use
|
|
||||||
Here is a non-exhaustive list of non permitted uses of the marks:
|
|
||||||
|
|
||||||
Usage of the Elixir logo in book covers, conferences, and podcasts.
|
|
||||||
|
|
||||||
Usage of the Elixir logo as the mark of third party projects, even in combination
|
|
||||||
with other marks.
|
|
||||||
|
|
||||||
Naming any company or product after Elixir, such as “The Elixir Hosting”,
|
|
||||||
“The Elixir Consultants”, etc.
|
|
||||||
|
|
||||||
Examples that require permission
|
|
||||||
Here are some examples that may be granted permission upon request:
|
|
||||||
|
|
||||||
Selling merchandise (stickers, t-shirts, mugs, etc).
|
|
||||||
You can request permission by emailing trademarks@elixir-lang.org.
|
|
||||||
|
|
||||||
Important note
|
|
||||||
Nothing in this page shall be interpreted to allow any third party to claim any
|
|
||||||
association with the Elixir project and the Elixir Team, or to imply any
|
|
||||||
approval or support by the Elixir project and the Elixir Team for any third
|
|
||||||
party products, services, or events.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
|
|
||||||
|
|
||||||
Unicode Data Files include all data files under the directories
|
|
||||||
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
|
|
||||||
http://www.unicode.org/cldr/data/ . Unicode Software includes any source
|
|
||||||
code published in the Unicode Standard or under the directories
|
|
||||||
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
|
|
||||||
http://www.unicode.org/cldr/data/.
|
|
||||||
|
|
||||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
|
||||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA
|
|
||||||
FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY
|
|
||||||
ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS
|
|
||||||
AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE
|
|
||||||
OR USE THE DATA FILES OR SOFTWARE.
|
|
||||||
|
|
||||||
COPYRIGHT AND PERMISSION NOTICE
|
|
||||||
|
|
||||||
Copyright © Unicode, Inc. All rights reserved. Distributed under
|
|
||||||
the Terms of Use in http://www.unicode.org/copyright.html.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
|
||||||
copy of the Unicode data files and any associated documentation (the
|
|
||||||
"Data Files") or Unicode software and any associated documentation (the
|
|
||||||
"Software") to deal in the Data Files or Software without restriction,
|
|
||||||
including without limitation the rights to use, copy, modify, merge,
|
|
||||||
publish, distribute, and/or sell copies of the Data Files or Software,
|
|
||||||
and to permit persons to whom the Data Files or Software are furnished
|
|
||||||
to do so, provided that
|
|
||||||
|
|
||||||
(a) the above copyright notice(s) and this permission notice appear with
|
|
||||||
all copies of the Data Files or Software,
|
|
||||||
|
|
||||||
(b) both the above copyright notice(s) and this permission notice appear
|
|
||||||
in associated documentation, and
|
|
||||||
|
|
||||||
(c) there is clear notice in each modified Data File or in the Software
|
|
||||||
as well as in the documentation associated with the Data File(s) or
|
|
||||||
Software that the data or software has been modified.
|
|
||||||
|
|
||||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
||||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
||||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR
|
|
||||||
ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
|
|
||||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
|
||||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
||||||
CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
|
||||||
|
|
||||||
Except as contained in this notice, the name of a copyright holder shall
|
|
||||||
not be used in advertising or otherwise to promote the sale, use or
|
|
||||||
other dealings in these Data Files or Software without prior written
|
|
||||||
authorization of the copyright holder.
|
|
||||||
|
|
||||||
Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be
|
|
||||||
registered in some jurisdictions. All other trademarks and registered
|
|
||||||
trademarks mentioned herein are the property of their respective owners.
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
TEST_FILES ?= "*_test.exs"
|
TEST_FILES ?= "*_test.exs"
|
||||||
SHARE_PREFIX ?= $(PREFIX)/share
|
SHARE_PREFIX ?= $(PREFIX)/share
|
||||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||||
CANONICAL := main/
|
CANONICAL := 1.15/
|
||||||
|
CANONICAL ?= main/
|
||||||
|
DOCS_FORMAT ?= html
|
||||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||||
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
|
|
||||||
ERLC := erlc -I lib/elixir/include
|
ERLC := erlc -I lib/elixir/include
|
||||||
ERL_MAKE := erl -make
|
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||||
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
|
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
|
||||||
VERSION := $(strip $(shell cat VERSION))
|
VERSION := $(strip $(shell cat VERSION))
|
||||||
@@ -22,19 +19,19 @@ INSTALL_DIR = $(INSTALL) -m755 -d
|
|||||||
INSTALL_DATA = $(INSTALL) -m644
|
INSTALL_DATA = $(INSTALL) -m644
|
||||||
INSTALL_PROGRAM = $(INSTALL) -m755
|
INSTALL_PROGRAM = $(INSTALL) -m755
|
||||||
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
|
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
|
||||||
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
|
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
|
||||||
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
|
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
|
||||||
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||||
|
|
||||||
.PHONY: cover install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
|
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
|
||||||
.NOTPARALLEL:
|
.NOTPARALLEL:
|
||||||
|
|
||||||
#==> Functions
|
#==> Functions
|
||||||
|
|
||||||
define CHECK_ERLANG_RELEASE
|
define CHECK_ERLANG_RELEASE
|
||||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -s erlang halt | grep -q '^true'; \
|
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \
|
||||||
if [ $$? != 0 ]; then \
|
if [ $$? != 0 ]; then \
|
||||||
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \
|
echo "At least Erlang/OTP 24.0 is required to build Elixir"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
endef
|
endef
|
||||||
@@ -50,13 +47,9 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
|
|||||||
@ rm -rf lib/$(1)/ebin
|
@ rm -rf lib/$(1)/ebin
|
||||||
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
|
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
|
||||||
|
|
||||||
test_$(1): test_formatted $(1)
|
test_$(1): compile $(1)
|
||||||
@ echo "==> $(1) (ex_unit)"
|
@ echo "==> $(1) (ex_unit)"
|
||||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
|
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
|
||||||
|
|
||||||
cover/ex_unit_$(1).coverdata:
|
|
||||||
$(Q) COVER="1" $(MAKE) test_$(1)
|
|
||||||
cover/combined.coverdata: cover/ex_unit_$(1).coverdata
|
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define WRITE_SOURCE_DATE_EPOCH
|
define WRITE_SOURCE_DATE_EPOCH
|
||||||
@@ -74,21 +67,17 @@ endef
|
|||||||
#==> Compilation tasks
|
#==> Compilation tasks
|
||||||
|
|
||||||
APP := lib/elixir/ebin/elixir.app
|
APP := lib/elixir/ebin/elixir.app
|
||||||
EEX := lib/eex/ebin/Elixir.EEx.beam
|
|
||||||
ELIXIR := lib/elixir/ebin/elixir.beam
|
|
||||||
PARSER := lib/elixir/src/elixir_parser.erl
|
PARSER := lib/elixir/src/elixir_parser.erl
|
||||||
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
|
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
|
||||||
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
|
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
|
||||||
|
|
||||||
default: compile
|
default: compile
|
||||||
|
|
||||||
compile: erlang elixir
|
compile: erlang $(APP) elixir
|
||||||
|
|
||||||
erlang: $(ELIXIR)
|
erlang: $(PARSER)
|
||||||
$(ELIXIR): $(PARSER) lib/elixir/src/*
|
|
||||||
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
||||||
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
||||||
$(Q) $(GENERATE_APP) $(VERSION)
|
|
||||||
|
|
||||||
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||||
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
||||||
@@ -96,29 +85,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
|
|||||||
# Since Mix depends on EEx and EEx depends on Mix,
|
# Since Mix depends on EEx and EEx depends on Mix,
|
||||||
# we first compile EEx without the .app file,
|
# we first compile EEx without the .app file,
|
||||||
# then Mix, and then compile EEx fully
|
# then Mix, and then compile EEx fully
|
||||||
elixir: stdlib $(EEX) mix ex_unit logger eex iex
|
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
|
||||||
stdlib: $(KERNEL) $(UNICODE) $(APP)
|
|
||||||
|
|
||||||
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
|
stdlib: $(KERNEL) VERSION
|
||||||
|
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
|
||||||
$(Q) if [ ! -f $(KERNEL) ]; then \
|
$(Q) if [ ! -f $(KERNEL) ]; then \
|
||||||
echo "==> bootstrap (compile)"; \
|
echo "==> bootstrap (compile)"; \
|
||||||
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
|
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
|
||||||
"$(MAKE)" unicode; \
|
|
||||||
fi
|
fi
|
||||||
|
$(Q) "$(MAKE)" unicode
|
||||||
@ echo "==> elixir (compile)";
|
@ echo "==> elixir (compile)";
|
||||||
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
|
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||||
$(Q) $(GENERATE_APP) $(VERSION)
|
$(Q) "$(MAKE)" app
|
||||||
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
|
|
||||||
|
|
||||||
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
|
app: $(APP)
|
||||||
$(Q) $(GENERATE_APP) $(VERSION)
|
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||||
|
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
|
||||||
|
|
||||||
unicode: $(UNICODE)
|
unicode: $(UNICODE)
|
||||||
$(UNICODE): lib/elixir/unicode/*
|
$(UNICODE): lib/elixir/unicode/*
|
||||||
@ echo "==> unicode (compile)";
|
@ echo "==> unicode (compile)";
|
||||||
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
|
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
|
||||||
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
|
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
|
||||||
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
|
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
|
||||||
|
|
||||||
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
|
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
|
||||||
$(eval $(call APP_TEMPLATE,logger,Logger))
|
$(eval $(call APP_TEMPLATE,logger,Logger))
|
||||||
@@ -157,7 +146,6 @@ check_reproducible: compile
|
|||||||
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
|
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
|
||||||
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
|
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
|
||||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||||
$(Q) rm -rf lib/*/ebin
|
|
||||||
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
|
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
|
||||||
$(Q) echo "Diffing..."
|
$(Q) echo "Diffing..."
|
||||||
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||||
@@ -168,10 +156,16 @@ check_reproducible: compile
|
|||||||
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||||
$(Q) echo "Builds are reproducible"
|
$(Q) echo "Builds are reproducible"
|
||||||
|
|
||||||
clean: clean_man
|
clean:
|
||||||
rm -rf ebin
|
rm -rf ebin
|
||||||
rm -rf lib/*/ebin
|
rm -rf lib/*/ebin
|
||||||
rm -rf $(PARSER)
|
rm -rf $(PARSER)
|
||||||
|
$(Q) "$(MAKE)" clean_residual_files
|
||||||
|
|
||||||
|
clean_elixir:
|
||||||
|
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||||
|
|
||||||
|
clean_residual_files:
|
||||||
rm -rf lib/*/_build/
|
rm -rf lib/*/_build/
|
||||||
rm -rf lib/*/tmp/
|
rm -rf lib/*/tmp/
|
||||||
rm -rf lib/elixir/test/ebin/
|
rm -rf lib/elixir/test/ebin/
|
||||||
@@ -181,15 +175,13 @@ clean: clean_man
|
|||||||
rm -rf lib/mix/test/fixtures/git_sparse_repo/
|
rm -rf lib/mix/test/fixtures/git_sparse_repo/
|
||||||
rm -rf lib/mix/test/fixtures/archive/ebin/
|
rm -rf lib/mix/test/fixtures/archive/ebin/
|
||||||
rm -f erl_crash.dump
|
rm -f erl_crash.dump
|
||||||
rm -rf cover
|
$(Q) "$(MAKE)" clean_man
|
||||||
|
|
||||||
clean_elixir:
|
|
||||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
|
||||||
|
|
||||||
#==> Documentation tasks
|
#==> Documentation tasks
|
||||||
|
|
||||||
|
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
|
||||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||||
|
|
||||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||||
@@ -231,19 +223,19 @@ docs_logger: compile ../ex_doc/bin/ex_doc
|
|||||||
$(call DOCS_CONFIG,logger)
|
$(call DOCS_CONFIG,logger)
|
||||||
|
|
||||||
../ex_doc/bin/ex_doc:
|
../ex_doc/bin/ex_doc:
|
||||||
@ echo "ex_doc is not found in ../ex_doc as expected. See CONTRIBUTING.md for more information."
|
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||||
@ false
|
@ false
|
||||||
|
|
||||||
#==> Zip tasks
|
#==> Zip tasks
|
||||||
|
|
||||||
Docs.zip: docs
|
Docs.zip: docs
|
||||||
rm -f Docs.zip
|
rm -f Docs.zip
|
||||||
zip -9 -r Docs.zip CHANGELOG.md doc LICENSE README.md
|
zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
|
||||||
@ echo "Docs file created $(CURDIR)/Docs.zip"
|
@ echo "Docs file created $(CURDIR)/Docs.zip"
|
||||||
|
|
||||||
Precompiled.zip: build_man compile
|
Precompiled.zip: build_man compile
|
||||||
rm -f Precompiled.zip
|
rm -f Precompiled.zip
|
||||||
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man README.md VERSION
|
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
|
||||||
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
|
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
|
||||||
|
|
||||||
#==> Test tasks
|
#==> Test tasks
|
||||||
@@ -294,15 +286,6 @@ test_stdlib: compile
|
|||||||
cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
|
cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cover/ex_unit_elixir.coverdata:
|
|
||||||
$(Q) COVER="1" $(MAKE) test_stdlib
|
|
||||||
cover/combined.coverdata: cover/ex_unit_elixir.coverdata
|
|
||||||
|
|
||||||
cover/combined.coverdata:
|
|
||||||
bin/elixir ./lib/elixir/scripts/cover.exs
|
|
||||||
|
|
||||||
cover: cover/combined.coverdata
|
|
||||||
|
|
||||||
#==> Dialyzer tasks
|
#==> Dialyzer tasks
|
||||||
|
|
||||||
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs
|
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs
|
||||||
@@ -325,15 +308,17 @@ dialyze: compile $(PLT)
|
|||||||
|
|
||||||
build_man: man/iex.1 man/elixir.1
|
build_man: man/iex.1 man/elixir.1
|
||||||
|
|
||||||
define BUILD_MANPAGES
|
man/iex.1:
|
||||||
man/$(APP).1:
|
$(Q) cp man/iex.1.in man/iex.1
|
||||||
$(Q) cp man/$(APP).1.in man/$(APP).1
|
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
|
||||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
|
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
|
||||||
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
|
$(Q) rm -f man/iex.1.bak
|
||||||
$(Q) rm -f man/$(APP).1.bak
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
|
man/elixir.1:
|
||||||
|
$(Q) cp man/elixir.1.in man/elixir.1
|
||||||
|
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
|
||||||
|
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
|
||||||
|
$(Q) rm -f man/elixir.1.bak
|
||||||
|
|
||||||
clean_man:
|
clean_man:
|
||||||
rm -f man/elixir.1
|
rm -f man/elixir.1
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
LEGAL NOTICE INFORMATION
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
All the files in this distribution are copyright to the terms below.
|
||||||
|
|
||||||
|
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
|
||||||
|
|
||||||
|
Copyright Ericsson AB 1996-2015
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
== All other files
|
||||||
|
|
||||||
|
Copyright 2012 Plataformatec
|
||||||
|
Copyright 2021 The Elixir Team
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
<!--
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Open Source Policy
|
|
||||||
|
|
||||||
## 1. Introduction
|
|
||||||
|
|
||||||
This Open Source Policy outlines the licensing, contribution, and compliance
|
|
||||||
requirements for all code released under the Elixir project. By adhering to
|
|
||||||
these guidelines, we ensure that our community, maintainers, and contributors
|
|
||||||
uphold both legal and ethical standards while fostering a collaborative,
|
|
||||||
transparent environment.
|
|
||||||
|
|
||||||
This policy exists to support and protect the Elixir community. It aims to
|
|
||||||
balance openness, collaboration, and respect for all contributors’ rights,
|
|
||||||
ensuring that Elixir remains a trusted and innovative open source project.
|
|
||||||
|
|
||||||
## 2. Scope
|
|
||||||
|
|
||||||
This policy applies to the Elixir Programming language, located at
|
|
||||||
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
|
|
||||||
made, including documentation and any associated assets.
|
|
||||||
|
|
||||||
## 3. Licensing
|
|
||||||
|
|
||||||
All code released by the Elixir team is licensed under the
|
|
||||||
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
|
|
||||||
licenses are recognized as permissible in this project:
|
|
||||||
|
|
||||||
- The Unicode license, as documented at
|
|
||||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
|
||||||
|
|
||||||
- The Elixir Trademark Policy, as documented at
|
|
||||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
|
||||||
|
|
||||||
These licenses are considered acceptable for any files or code that form part of
|
|
||||||
an Elixir repository. If a contribution requires a different license, it must
|
|
||||||
either be rejected or prompt an update to this policy.
|
|
||||||
|
|
||||||
## 4. Contributing to the Elixir repository
|
|
||||||
|
|
||||||
Any code contributed to the Elixir repository must fall under one of the accepted
|
|
||||||
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
|
|
||||||
other license will be rejected unless this policy is formally revised to include
|
|
||||||
that license. All files except those specifically exempted (e.g., certain test
|
|
||||||
fixture files) must contain SPDX license and copyright headers
|
|
||||||
(`SPDX-License-Identifier` and `SPDX-FileCopyrightText`). If a file qualifies
|
|
||||||
for an exception, this must be configured in the ORT (Open Source Review Toolkit)
|
|
||||||
configuration and undergo review.
|
|
||||||
|
|
||||||
Contributions must not introduce executable binary files into the codebase.
|
|
||||||
|
|
||||||
## 5. Preservation of Copyright and License Information
|
|
||||||
|
|
||||||
Any third-party code incorporated into the Elixir repository must retain original
|
|
||||||
copyright and license headers. If no such headers exist in the source, they must
|
|
||||||
be added. This practice ensures that original authors receive proper credit and
|
|
||||||
that the licensing lineage is preserved.
|
|
||||||
|
|
||||||
## 6. Objectives
|
|
||||||
|
|
||||||
The Elixir project aims to promote a culture of responsible open source usage.
|
|
||||||
Specifically, our objectives include:
|
|
||||||
|
|
||||||
### 6.1 Clearly Define and Communicate Licensing & Compliance Policies
|
|
||||||
|
|
||||||
We will identify and document all third-party dependencies, ensure that license
|
|
||||||
information is communicated clearly, and maintain a project-wide license policy
|
|
||||||
or compliance handbook.
|
|
||||||
|
|
||||||
### 6.2 Implement Clear Processes for Reviewing Contributions
|
|
||||||
|
|
||||||
We will provide well-defined contribution guidelines. We implement the
|
|
||||||
Developer Certificate of Origin (DCO) for additional clarity regarding
|
|
||||||
contributor rights and obligations.
|
|
||||||
|
|
||||||
### 6.3 Track and Audit Third-Party Code Usage
|
|
||||||
|
|
||||||
All projects will implement a Software Bill of Materials (SBoM) strategy and
|
|
||||||
regularly verify license compliance for direct and transitive dependencies.
|
|
||||||
|
|
||||||
### 6.4 Monitor and Continuously Improve Open Source Compliance
|
|
||||||
|
|
||||||
We will conduct periodic internal audits, integrate compliance checks into
|
|
||||||
continuous integration (CI/CD) pipelines, and regularly review and refine these
|
|
||||||
objectives to align with best practices.
|
|
||||||
|
|
||||||
## 7. Roles and Responsibilities
|
|
||||||
|
|
||||||
### 7.1 Core Team Member
|
|
||||||
|
|
||||||
Core Team Members are responsible for being familiar with this policy and
|
|
||||||
ensuring it is consistently enforced. They must demonstrate sufficient
|
|
||||||
competencies to understand the policy requirements and must reject or request
|
|
||||||
changes to any pull requests that violate these standards.
|
|
||||||
|
|
||||||
### 7.2 Contributor
|
|
||||||
|
|
||||||
Contributors are expected to follow this policy when submitting code. If a
|
|
||||||
contributor submits a pull request that does not comply with the policy
|
|
||||||
(e.g., introduces a disallowed license), Core Team Members have the authority to
|
|
||||||
reject it or request changes. No special competencies are required for
|
|
||||||
contributors beyond awareness and adherence to the policy.
|
|
||||||
|
|
||||||
### 7.3 EEF CISO
|
|
||||||
|
|
||||||
The CISO designated by the Erlang Ecosystem Foundation (EEF) provides oversight
|
|
||||||
on queries and guidance regarding open source compliance or legal matters for
|
|
||||||
Elixir. The CISO is responsible for checking ongoing compliance with the policy,
|
|
||||||
escalating potential violations to the Core Team, and involving legal counsel if
|
|
||||||
necessary. This role does not require legal expertise but does involve
|
|
||||||
initiating legal or community discussions when needed.
|
|
||||||
|
|
||||||
## 8. Implications of Failing to Follow the Program Requirements
|
|
||||||
|
|
||||||
If a violation of this policy is identified, the Elixir Core Team will undertake
|
|
||||||
the following actions:
|
|
||||||
|
|
||||||
## 8.1 Review the Codebase for Additional Violations
|
|
||||||
|
|
||||||
We will investigate the codebase thoroughly to detect any similar instances of
|
|
||||||
non-compliance.
|
|
||||||
|
|
||||||
## 8.2 Review and Update the Process or Policy
|
|
||||||
|
|
||||||
In collaboration with the EEF CISO, the Elixir Core Team will assess the policy
|
|
||||||
and our internal workflows, making any necessary clarifications or amendments to
|
|
||||||
reduce the likelihood of recurrence.
|
|
||||||
|
|
||||||
## 8.3 Notify and Train Core Team Members
|
|
||||||
|
|
||||||
We will ensure that all active Core Team Members are informed about any policy
|
|
||||||
changes and understand how to apply them in everyday development.
|
|
||||||
|
|
||||||
## 8.4 Remove or Replace the Offending Code
|
|
||||||
|
|
||||||
If required, we will remove or replace the non-compliant code.
|
|
||||||
|
|
||||||
## 9. Contact
|
|
||||||
|
|
||||||
The project maintains a private mailing list at
|
|
||||||
[policy@elixir-lang.org](mailto:policy@elixir-lang.org) for handling licensing
|
|
||||||
and policy-related queries. Email is the preferred communication channel, and
|
|
||||||
the EEF CISO will be included on this list to provide assistance and ensure
|
|
||||||
timely responses. While solutions may take longer to implement, the project
|
|
||||||
commits to acknowledging all queries within five business days.
|
|
||||||
|
|
||||||
## 10. External Contributions of Core Team Members
|
|
||||||
|
|
||||||
When Core Team Members contribute to repositories outside Elixir, they do so in
|
|
||||||
a personal capacity or via their employer. They will not act as official
|
|
||||||
representatives of the Elixir team in those external contexts.
|
|
||||||
|
|
||||||
## 11. Policy Review and Amendments
|
|
||||||
|
|
||||||
This policy will be revisited annually to address new concerns, accommodate
|
|
||||||
changes in community standards, or adjust to emerging legal or technical
|
|
||||||
requirements. Proposed amendments must be reviewed by the Core Team and, if
|
|
||||||
necessary, by the EEF CISO. Any significant changes will be communicated to
|
|
||||||
contributors and made publicly available.
|
|
||||||
|
|
||||||
*Effective Date: 2025-02-20*
|
|
||||||
*Last Reviewed: 2025-11-20*
|
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
<!--
|
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
|
||||||
SPDX-License-Identifier: Apache-2.0
|
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
<h1>
|
[](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI)
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
|
|
||||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200">
|
|
||||||
</picture>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
[](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
|
Elixir is a dynamic, functional language designed for building scalable
|
||||||
[](https://www.bestpractices.dev/projects/10187)
|
|
||||||
|
|
||||||
Elixir is a programming language designed for building scalable
|
|
||||||
and maintainable applications.
|
and maintainable applications.
|
||||||
|
|
||||||
For more about Elixir, installation and documentation,
|
For more about Elixir, installation and documentation,
|
||||||
@@ -23,7 +12,7 @@ For more about Elixir, installation and documentation,
|
|||||||
## Policies
|
## Policies
|
||||||
|
|
||||||
New releases are announced in the [announcement mailing list][8].
|
New releases are announced in the [announcement mailing list][8].
|
||||||
You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com>
|
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
|
||||||
and replying to the confirmation email.
|
and replying to the confirmation email.
|
||||||
|
|
||||||
All security releases [will be tagged with `[security]`][10]. For more
|
All security releases [will be tagged with `[security]`][10]. For more
|
||||||
@@ -32,64 +21,61 @@ information, please read our [Security Policy][9].
|
|||||||
All interactions in our official communication channels follow our
|
All interactions in our official communication channels follow our
|
||||||
[Code of Conduct][1].
|
[Code of Conduct][1].
|
||||||
|
|
||||||
All contributions are required to conform to our [Open Source Policy][11].
|
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
For reporting bugs, [visit our issue tracker][2] and follow the steps
|
For reporting bugs, [visit our issue tracker][2] and follow the steps
|
||||||
for reporting a new issue. **Please disclose security vulnerabilities
|
for reporting a new issue. **Please disclose security vulnerabilities
|
||||||
privately [in our Security page](https://github.com/elixir-lang/elixir/security)**.
|
privately at elixir-security@googlegroups.com**.
|
||||||
|
|
||||||
All currently open bugs related to Elixir are listed in the issues tracker.
|
## Issues tracker management
|
||||||
The Elixir team uses the issues tracker to focus on *actionable items*,
|
|
||||||
including planned enhancements in the short and medium term. We also do
|
All currently open bugs related to the Elixir repository are listed
|
||||||
our best to label entries for clarity and to ease collaboration.
|
in the issues tracker. The Elixir team uses the issues tracker to focus
|
||||||
|
on *actionable items*, including planned enhancements in the short- and
|
||||||
|
medium-term. We also do our best to label entries for clarity and to ease
|
||||||
|
collaboration.
|
||||||
|
|
||||||
Our *actionable item policy* has some important consequences, such as:
|
Our *actionable item policy* has some important consequences, such as:
|
||||||
|
|
||||||
* Proposing new features as well as requests for support, help, and
|
* Proposing new features as well as request for support, help, and
|
||||||
guidance must be done in their own spaces, detailed next.
|
guidance must be done in their own spaces, detailed next.
|
||||||
|
|
||||||
* Issues we have identified to be outside of Elixir's scope,
|
* Issues where we have identified to be outside of Elixir scope,
|
||||||
such as an upstream bug, will be closed (and requested to be moved
|
such as a bug upstream, will be closed (and requested to be moved
|
||||||
elsewhere if appropriate).
|
elsewhere if appropriate).
|
||||||
|
|
||||||
* We actively close unrelated and non-actionable issues to keep the
|
* We actively close unrelated and non-actionable issues to keep the
|
||||||
issues tracker tidy. If you believe we got something wrong, drop a
|
issues tracker tidy. However, we may get things wrong from time to
|
||||||
comment and we can always reopen the issue.
|
time, so we are glad to revisit issues and reopen if necessary.
|
||||||
|
|
||||||
By keeping the overall issues tracker tidy and organized, the community
|
Keep the tone positive and be kind! For more information, see the
|
||||||
can easily peek at what is coming in new releases and also get involved
|
[Code of Conduct][1].
|
||||||
by commenting on existing issues and submitting pull requests. Please
|
|
||||||
remember to keep the tone positive and be kind! For more information,
|
|
||||||
see the [Code of Conduct][1].
|
|
||||||
|
|
||||||
## Discussions, support, and help
|
### Proposing new features
|
||||||
|
|
||||||
For general discussions, support, and help, please use the community
|
For proposing new features, please start a discussion in the
|
||||||
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
|
[Elixir Core mailing list][3]. The [language development history and
|
||||||
such as forums, chat platforms, etc, where the wider community will be available
|
its focus are described on our website](https://elixir-lang.org/development.html).
|
||||||
to help you.
|
|
||||||
|
|
||||||
## Proposing new features
|
Keep in mind that it is your responsibility to argue and explain
|
||||||
|
why a feature is useful and how it will impact the codebase and
|
||||||
We encourage you to first propose new features in the community spaces
|
the community. A good proposal includes the problem description
|
||||||
listed above. These discussions help refine ideas and gather feedback before
|
and how the proposed solution compares with existing alternatives
|
||||||
submission. Our website also includes [a general outline of the language
|
in the Elixir ecosystem (as well as in other languages). To iron
|
||||||
history and its current development focus](https://elixir-lang.org/development.html).
|
out a proposal before submission, consider using and gathering
|
||||||
|
feedback from the community spaces [listed on the sidebar of the
|
||||||
Once you are ready, you can submit your proposal to the [Elixir Core
|
Elixir website](https://elixir-lang.org/).
|
||||||
mailing list][3], either through the web interface or by subscribing to
|
|
||||||
it at <elixir-lang-core+subscribe@googlegroups.com>. Remember to include
|
|
||||||
a clear problem description, compare the proposed solution to existing
|
|
||||||
alternatives in the Elixir ecosystem (and in other languages if possible),
|
|
||||||
and consider the potential impact your changes will have on the codebase and
|
|
||||||
community.
|
|
||||||
|
|
||||||
Once a proposal is accepted, it will be added to [the issue tracker][2].
|
Once a proposal is accepted, it will be added to [the issue tracker][2].
|
||||||
Features and bug fixes that have already been merged and will be included
|
Features and bug fixes that have already been merged and will be included
|
||||||
in the next release are then "closed" and added to the [changelog][7]
|
in the next release are then "closed" and added to the [changelog][7].
|
||||||
before release.
|
|
||||||
|
### Discussions, support, and help
|
||||||
|
|
||||||
|
For general discussions, support, and help, please use many of the community
|
||||||
|
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
|
||||||
|
such as forums, chat platforms, etc, where the wider community will be available
|
||||||
|
to help you.
|
||||||
|
|
||||||
## Compiling from source
|
## Compiling from source
|
||||||
|
|
||||||
@@ -103,7 +89,7 @@ After that, clone this repository to your machine, compile and test it:
|
|||||||
```sh
|
```sh
|
||||||
git clone https://github.com/elixir-lang/elixir.git
|
git clone https://github.com/elixir-lang/elixir.git
|
||||||
cd elixir
|
cd elixir
|
||||||
make
|
make clean test
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: if you are running on Windows,
|
> Note: if you are running on Windows,
|
||||||
@@ -113,19 +99,124 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
|
|||||||
In case you want to use this Elixir version as your system version,
|
In case you want to use this Elixir version as your system version,
|
||||||
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
|
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
|
||||||
|
|
||||||
When updating the repository, you may want to run `make clean` before
|
If Elixir fails to build (specifically when pulling in a new version via
|
||||||
recompiling. For deterministic builds, you should set the environment
|
`git`), be sure to remove any previous build artifacts by running
|
||||||
variable `ERL_COMPILER_OPTIONS=deterministic`.
|
`make clean`, then `make test`.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions to Elixir are always welcome! Before you get started, please check
|
We welcome everyone to contribute to Elixir. To do so, there are a few
|
||||||
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
|
things you need to know about the code. First, Elixir code is divided
|
||||||
guidelines on how to set up your environment, run the test suite, format your
|
in applications inside the `lib` folder:
|
||||||
code, and submit pull requests.
|
|
||||||
|
|
||||||
Note you must disclose the use of coding agents and AI written code in your
|
* `elixir` - Elixir's kernel and standard library
|
||||||
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
* `eex` - EEx is the template engine that allows you to embed Elixir
|
||||||
|
|
||||||
|
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
|
||||||
|
|
||||||
|
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
|
||||||
|
|
||||||
|
* `logger` - Logger is the built-in logger
|
||||||
|
|
||||||
|
* `mix` - Mix is Elixir's build tool
|
||||||
|
|
||||||
|
You can run all tests in the root directory with `make test` and you can
|
||||||
|
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
|
||||||
|
`make test_ex_unit`. If you just changed something in Elixir's standard
|
||||||
|
library, you can run only that portion through `make test_stdlib`.
|
||||||
|
|
||||||
|
If you are changing just one file, you can choose to compile and run tests only
|
||||||
|
for that particular file for fast development cycles. For example, if you
|
||||||
|
are changing the String module, you can compile it and run its tests as:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
||||||
|
bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the `LINE` env var to run a single test:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||||
|
````
|
||||||
|
|
||||||
|
To recompile (including Erlang modules):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make compile
|
||||||
|
```
|
||||||
|
|
||||||
|
After your changes are done, please remember to run `make format` to guarantee
|
||||||
|
all files are properly formatted and then run the full suite with
|
||||||
|
`make test`.
|
||||||
|
|
||||||
|
If your contribution fails during the bootstrapping of the language,
|
||||||
|
you can rebuild the language from scratch with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make clean_elixir compile
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, if you can't get Elixir to compile or the tests to pass after
|
||||||
|
updating an existing checkout, run `make clean compile`. You can check
|
||||||
|
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
||||||
|
More tasks can be found by reading the [Makefile](Makefile).
|
||||||
|
|
||||||
|
With tests running and passing, you are ready to contribute to Elixir and
|
||||||
|
[send a pull request](https://help.github.com/articles/using-pull-requests/).
|
||||||
|
We have saved some excellent pull requests we have received in the past in
|
||||||
|
case you are looking for some examples:
|
||||||
|
|
||||||
|
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||||
|
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||||
|
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||||
|
|
||||||
|
### Reviewing changes
|
||||||
|
|
||||||
|
Once a pull request is sent, the Elixir team will review your changes.
|
||||||
|
We outline our process below to clarify the roles of everyone involved.
|
||||||
|
|
||||||
|
All pull requests must be approved by two committers before being merged into
|
||||||
|
the repository. If any changes are necessary, the team will leave appropriate
|
||||||
|
comments requesting changes to the code. Unfortunately, we cannot guarantee a
|
||||||
|
pull request will be merged, even when modifications are requested, as the Elixir
|
||||||
|
team will re-evaluate the contribution as it changes.
|
||||||
|
|
||||||
|
Committers may also push style changes directly to your branch. If you would
|
||||||
|
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
|
||||||
|
feature when submitting your pull request.
|
||||||
|
|
||||||
|
The Elixir team may optionally assign someone to review a pull request.
|
||||||
|
If someone is assigned, they must explicitly approve the code before
|
||||||
|
another team member can merge it.
|
||||||
|
|
||||||
|
When the review finishes, your pull request will be squashed and merged
|
||||||
|
into the repository. If you have carefully organized your commits and
|
||||||
|
believe they should be merged without squashing, please mention it in
|
||||||
|
a comment.
|
||||||
|
|
||||||
|
## Building documentation
|
||||||
|
|
||||||
|
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
to be installed and built alongside Elixir:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# After cloning and compiling Elixir, in its parent directory:
|
||||||
|
git clone https://github.com/elixir-lang/ex_doc.git
|
||||||
|
cd ex_doc && ../elixir/bin/mix do deps.get + compile
|
||||||
|
```
|
||||||
|
|
||||||
|
Now go back to Elixir's root directory and run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make docs # to generate HTML pages
|
||||||
|
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||||
|
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||||
|
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||||
|
|
||||||
## Development links
|
## Development links
|
||||||
|
|
||||||
@@ -148,7 +239,6 @@ contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIB
|
|||||||
[8]: https://groups.google.com/group/elixir-lang-ann
|
[8]: https://groups.google.com/group/elixir-lang-ann
|
||||||
[9]: SECURITY.md
|
[9]: SECURITY.md
|
||||||
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
|
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
|
||||||
[11]: OPEN_SOURCE_POLICY.md
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -156,4 +246,4 @@ contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIB
|
|||||||
|
|
||||||
Elixir source code is released under Apache License 2.0.
|
Elixir source code is released under Apache License 2.0.
|
||||||
|
|
||||||
Check [LICENSE](LICENSE) file for more information.
|
Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.
|
||||||
|
|||||||
+9
-17
@@ -1,34 +1,26 @@
|
|||||||
<!--
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Release process
|
# Release process
|
||||||
|
|
||||||
## Shipping a new version
|
## Shipping a new version
|
||||||
|
|
||||||
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
|
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
|
||||||
|
|
||||||
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
||||||
|
|
||||||
3. Update "Compatibility and Deprecations" if a new OTP version is supported
|
3. Update "Compatibility and Deprecations" if a new OTP version is supported
|
||||||
|
|
||||||
4. Commit changes above with title "Release vVERSION" and push it
|
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
|
||||||
|
|
||||||
5. Once GitHub actions completes, generate a new tag, and push it
|
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
|
||||||
|
|
||||||
6. Wait until GitHub Actions publish artifacts to the draft release
|
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
|
||||||
|
|
||||||
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
|
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||||
|
|
||||||
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
## Creating a new vMAJOR.MINOR branch (after first rc)
|
||||||
|
|
||||||
## Creating a new vMAJOR.MINOR branch (usually before first rc)
|
|
||||||
|
|
||||||
### In the new branch
|
### In the new branch
|
||||||
|
|
||||||
1. Comment out `CANONICAL := main/` in /Makefile
|
1. Set `CANONICAL=` in /Makefile
|
||||||
|
|
||||||
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
||||||
|
|
||||||
@@ -36,7 +28,7 @@
|
|||||||
|
|
||||||
### Back in main
|
### Back in main
|
||||||
|
|
||||||
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat
|
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
|
||||||
|
|
||||||
2. Start new /CHANGELOG.md
|
2. Start new /CHANGELOG.md
|
||||||
|
|
||||||
@@ -50,6 +42,6 @@
|
|||||||
|
|
||||||
2. Update `otp_release` checks in `/Makefile` and `/lib/elixir/src/elixir.erl`
|
2. Update `otp_release` checks in `/Makefile` and `/lib/elixir/src/elixir.erl`
|
||||||
|
|
||||||
3. Update relevant CI workflows in `/.github/workflows/*.yml` - for release workflows, outdated/recently added Erlang/OTP versions must run conditionally
|
3. Update relevant CI workflows in `/.github/workflows/*.yml`
|
||||||
|
|
||||||
4. Remove `otp_release` version checks that are no longer needed
|
4. Remove `otp_release` version checks that are no longer needed
|
||||||
|
|||||||
+8
-15
@@ -1,9 +1,3 @@
|
|||||||
<!--
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
## Supported versions
|
## Supported versions
|
||||||
@@ -12,19 +6,18 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
|||||||
|
|
||||||
Elixir version | Support
|
Elixir version | Support
|
||||||
:------------- | :-----------------------------
|
:------------- | :-----------------------------
|
||||||
1.21 | Development
|
1.15 | Bug fixes and security patches
|
||||||
1.20 | Bug fixes and security patches
|
1.14 | Security patches only
|
||||||
1.19 | Security patches only
|
1.13 | Security patches only
|
||||||
1.18 | Security patches only
|
1.12 | Security patches only
|
||||||
1.17 | Security patches only
|
1.11 | Security patches only
|
||||||
1.16 | Security patches only
|
|
||||||
|
|
||||||
## Announcements
|
## Announcements
|
||||||
|
|
||||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email.
|
||||||
|
|
||||||
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
|
Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## Reporting a vulnerability
|
||||||
|
|
||||||
[Please disclose security vulnerabilities privately via GitHub](https://github.com/elixir-lang/elixir/security).
|
Please disclose security vulnerabilities privately at elixir-security@googlegroups.com
|
||||||
|
|||||||
+12
-10
@@ -1,12 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
ELIXIR_VERSION=1.21.0-dev
|
ELIXIR_VERSION=1.15.8
|
||||||
|
|
||||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||||
cat <<USAGE >&2
|
cat <<USAGE >&2
|
||||||
@@ -23,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
|
|||||||
-pz "PATH" Appends the given path to Erlang code path (*)
|
-pz "PATH" Appends the given path to Erlang code path (*)
|
||||||
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||||
|
|
||||||
--color, --no-color Enables or disables ANSI coloring
|
|
||||||
--erl "SWITCHES" Switches to be passed down to Erlang (*)
|
--erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||||
--eval "COMMAND" Evaluates the given command, same as -e (*)
|
--eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||||
--logger-otp-reports BOOL Enables or disables OTP reporting
|
--logger-otp-reports BOOL Enables or disables OTP reporting
|
||||||
--logger-sasl-reports BOOL Enables or disables SASL reporting
|
--logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||||
--no-halt Does not halt the Erlang VM after execution
|
--no-halt Does not halt the Erlang VM after execution
|
||||||
--short-version Prints Elixir version (standalone)
|
--short-version Prints Elixir version (standalone)
|
||||||
|
--werl Uses Erlang's Windows shell GUI (Windows only)
|
||||||
|
|
||||||
Options given after the .exs file or -- are passed down to the executed code.
|
Options given after the .exs file or -- are passed down to the executed code.
|
||||||
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
|
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
|
||||||
@@ -117,7 +112,7 @@ while [ $I -le $LENGTH ]; do
|
|||||||
C=1
|
C=1
|
||||||
MODE="iex"
|
MODE="iex"
|
||||||
;;
|
;;
|
||||||
-v|--no-halt|--color|--no-color)
|
-v|--no-halt)
|
||||||
C=1
|
C=1
|
||||||
;;
|
;;
|
||||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
|
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
|
||||||
@@ -187,6 +182,10 @@ while [ $I -le $LENGTH ]; do
|
|||||||
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
|
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
--werl)
|
||||||
|
S=1
|
||||||
|
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
while [ $I -le $LENGTH ]; do
|
while [ $I -le $LENGTH ]; do
|
||||||
I=$((I + 1))
|
I=$((I + 1))
|
||||||
@@ -219,14 +218,17 @@ SELF=$(readlink_f "$0")
|
|||||||
SCRIPT_PATH=$(dirname "$SELF")
|
SCRIPT_PATH=$(dirname "$SELF")
|
||||||
|
|
||||||
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
|
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
|
||||||
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
|
|
||||||
|
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
|
||||||
|
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
# One MAY change ERTS_BIN= but you MUST NOT change
|
# One MAY change ERTS_BIN= but you MUST NOT change
|
||||||
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
|
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
|
||||||
ERTS_BIN=
|
ERTS_BIN=
|
||||||
ERTS_BIN="$ERTS_BIN"
|
ERTS_BIN="$ERTS_BIN"
|
||||||
|
|
||||||
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
|
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"
|
||||||
|
|
||||||
if [ -n "$RUN_ERL_PIPE" ]; then
|
if [ -n "$RUN_ERL_PIPE" ]; then
|
||||||
ESCAPED=""
|
ESCAPED=""
|
||||||
|
|||||||
+77
-39
@@ -1,11 +1,8 @@
|
|||||||
@echo off
|
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||||
|
|
||||||
:: SPDX-License-Identifier: Apache-2.0
|
set ELIXIR_VERSION=1.15.8
|
||||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
set ELIXIR_VERSION=1.21.0-dev
|
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||||
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
|
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
|
||||||
@@ -28,13 +25,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
|
|||||||
echo -pz "PATH" Appends the given path to Erlang code path (*)
|
echo -pz "PATH" Appends the given path to Erlang code path (*)
|
||||||
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||||
echo.
|
echo.
|
||||||
echo --color, --no-color Enables or disables ANSI coloring
|
|
||||||
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
|
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||||
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
|
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||||
echo --logger-otp-reports BOOL Enables or disables OTP reporting
|
echo --logger-otp-reports BOOL Enables or disables OTP reporting
|
||||||
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
|
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||||
echo --no-halt Does not halt the Erlang VM after execution
|
echo --no-halt Does not halt the Erlang VM after execution
|
||||||
echo --short-version Prints Elixir version (standalone)
|
echo --short-version Prints Elixir version (standalone)
|
||||||
|
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||||
echo.
|
echo.
|
||||||
echo Options given after the .exs file or -- are passed down to the executed code.
|
echo Options given after the .exs file or -- are passed down to the executed code.
|
||||||
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
|
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
|
||||||
@@ -67,11 +64,13 @@ echo ** Standalone options can't be combined with other options.
|
|||||||
goto end
|
goto end
|
||||||
|
|
||||||
:shortversion
|
:shortversion
|
||||||
echo %ELIXIR_VERSION%
|
echo !ELIXIR_VERSION!
|
||||||
goto end
|
goto end
|
||||||
|
|
||||||
:parseopts
|
:parseopts
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
rem Parameters for Elixir
|
||||||
|
set parsElixir=
|
||||||
|
|
||||||
rem Parameters for Erlang
|
rem Parameters for Erlang
|
||||||
set parsErlang=
|
set parsErlang=
|
||||||
@@ -98,51 +97,90 @@ if "!par!"=="" (
|
|||||||
)
|
)
|
||||||
shift
|
shift
|
||||||
set par="!par:"=\"!"
|
set par="!par:"=\"!"
|
||||||
|
if !endLoop! == 1 (
|
||||||
|
set parsElixir=!parsElixir! !par!
|
||||||
|
goto startloop
|
||||||
|
)
|
||||||
rem ******* EXECUTION OPTIONS **********************
|
rem ******* EXECUTION OPTIONS **********************
|
||||||
if !par!=="+iex" (set useIEx=1 && goto startloop)
|
if !par!=="--werl" (set useWerl=1 && goto startloop)
|
||||||
if !par!=="+elixirc" (goto startloop)
|
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set useIEx=1 && goto startloop)
|
||||||
|
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && goto startloop)
|
||||||
|
rem ******* EVAL PARAMETERS ************************
|
||||||
|
if ""==!par:-e=! (
|
||||||
|
set "VAR=%~1"
|
||||||
|
if not defined VAR (set VAR= )
|
||||||
|
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
|
||||||
|
shift
|
||||||
|
goto startloop
|
||||||
|
)
|
||||||
|
if ""==!par:--eval=! (
|
||||||
|
set "VAR=%~1"
|
||||||
|
if not defined VAR (set VAR= )
|
||||||
|
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
|
||||||
|
shift
|
||||||
|
goto startloop
|
||||||
|
)
|
||||||
|
if ""==!par:--rpc-eval=! (
|
||||||
|
set "VAR=%~2"
|
||||||
|
if not defined VAR (set VAR= )
|
||||||
|
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
goto startloop
|
||||||
|
)
|
||||||
rem ******* ELIXIR PARAMETERS **********************
|
rem ******* ELIXIR PARAMETERS **********************
|
||||||
if ""==!par:-e=! (shift && goto startloop)
|
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %~1" && shift && goto startloop)
|
||||||
if ""==!par:--eval=! (shift && goto startloop)
|
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %~1" && shift && goto startloop)
|
||||||
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
|
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %~1" && shift && goto startloop)
|
||||||
if ""==!par:-r=! (shift && goto startloop)
|
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %~1" && shift && goto startloop)
|
||||||
if ""==!par:-pr=! (shift && goto startloop)
|
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
|
||||||
if ""==!par:-pa=! (shift && goto startloop)
|
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
|
||||||
if ""==!par:-pz=! (shift && goto startloop)
|
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
|
||||||
if ""==!par:-v=! (goto startloop)
|
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && goto startloop)
|
||||||
if ""==!par:--version=! (goto startloop)
|
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
|
||||||
if ""==!par:--no-halt=! (goto startloop)
|
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
|
||||||
if ""==!par:--color=! (goto startloop)
|
|
||||||
if ""==!par:--no-color=! (goto startloop)
|
|
||||||
if ""==!par:--remsh=! (shift && goto startloop)
|
|
||||||
if ""==!par:--dot-iex=! (shift && goto startloop)
|
|
||||||
if ""==!par:--dbg=! (shift && goto startloop)
|
|
||||||
rem ******* ERLANG PARAMETERS **********************
|
rem ******* ERLANG PARAMETERS **********************
|
||||||
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
|
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %~1" && shift && goto startloop)
|
||||||
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
|
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %~1 %~2" && shift && shift && goto startloop)
|
||||||
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie "%~1"" && shift && goto startloop)
|
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %~1" && shift && goto startloop)
|
||||||
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
|
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
|
||||||
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config "%~1"" && shift && goto startloop)
|
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %~1" && shift && goto startloop)
|
||||||
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
|
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
|
||||||
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
|
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
|
||||||
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name "%~1"" && shift && goto startloop)
|
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %~1" && shift && goto startloop)
|
||||||
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1"" && shift && goto startloop)
|
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %~1" && shift && goto startloop)
|
||||||
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
|
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %~1" && shift && goto startloop)
|
||||||
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
|
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
|
||||||
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
|
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
|
||||||
|
set endLoop=1
|
||||||
|
set parsElixir=!parsElixir! !par!
|
||||||
|
goto startloop
|
||||||
|
|
||||||
:run
|
:run
|
||||||
setlocal disabledelayedexpansion
|
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
|
||||||
if not defined useIEx (
|
if %errorlevel% == 0 (
|
||||||
set beforeExtra=-s elixir start_cli %beforeExtra%
|
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
|
||||||
|
)
|
||||||
|
if defined useIEx (
|
||||||
|
set beforeExtra=-s elixir start_iex !beforeExtra!
|
||||||
|
) else (
|
||||||
|
set beforeExtra=-s elixir start_cli !beforeExtra!
|
||||||
)
|
)
|
||||||
|
|
||||||
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
|
set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra!
|
||||||
|
|
||||||
if defined ELIXIR_CLI_DRY_RUN (
|
if defined ELIXIR_CLI_DRY_RUN (
|
||||||
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
if defined useWerl (
|
||||||
|
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||||
|
) else (
|
||||||
|
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||||
|
)
|
||||||
) else (
|
) else (
|
||||||
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
if defined useWerl (
|
||||||
|
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||||
|
) else (
|
||||||
|
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exit /B %ERRORLEVEL%
|
exit /B %ERRORLEVEL%
|
||||||
:end
|
:end
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||||
|
|||||||
+1
-6
@@ -1,9 +1,4 @@
|
|||||||
@echo off
|
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||||
|
|
||||||
:: SPDX-License-Identifier: Apache-2.0
|
|
||||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
setlocal
|
setlocal
|
||||||
set argc=0
|
set argc=0
|
||||||
for %%A in (%*) do (
|
for %%A in (%*) do (
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||||
|
|||||||
+5
-7
@@ -1,9 +1,4 @@
|
|||||||
@echo off
|
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||||
|
|
||||||
:: SPDX-License-Identifier: Apache-2.0
|
|
||||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
setlocal
|
setlocal
|
||||||
if /I ""%1""==""--help"" goto documentation
|
if /I ""%1""==""--help"" goto documentation
|
||||||
if /I ""%1""==""-h"" goto documentation
|
if /I ""%1""==""-h"" goto documentation
|
||||||
@@ -21,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
|
|||||||
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
|
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
|
||||||
echo If FILE is empty, then no file will be loaded.
|
echo If FILE is empty, then no file will be loaded.
|
||||||
echo --remsh NAME Connects to a node using a remote shell
|
echo --remsh NAME Connects to a node using a remote shell
|
||||||
|
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||||
echo.
|
echo.
|
||||||
|
echo Set the IEX_WITH_WERL environment variable to always use werl.
|
||||||
echo It accepts all other options listed by "elixir --help".
|
echo It accepts all other options listed by "elixir --help".
|
||||||
goto end
|
goto end
|
||||||
|
|
||||||
:run
|
:run
|
||||||
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
|
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
|
||||||
|
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
|
||||||
:end
|
:end
|
||||||
endlocal
|
endlocal
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
#!/usr/bin/env elixir
|
#!/usr/bin/env elixir
|
||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
Mix.CLI.main()
|
Mix.CLI.main()
|
||||||
|
|||||||
+1
-6
@@ -1,7 +1,2 @@
|
|||||||
@echo off
|
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||||
|
|
||||||
:: SPDX-License-Identifier: Apache-2.0
|
|
||||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
call "%~dp0\elixir.bat" "%~dp0\mix" %*
|
call "%~dp0\elixir.bat" "%~dp0\mix" %*
|
||||||
|
|||||||
Executable → Regular
+1
-5
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
# Store path to mix.bat as a FileInfo object
|
# Store path to mix.bat as a FileInfo object
|
||||||
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
|
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
|
||||||
$newArgs = @()
|
$newArgs = @()
|
||||||
@@ -24,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Corrected arguments are ready to pass to batch file
|
# Corrected arguments are ready to pass to batch file
|
||||||
& $mixBatPath $newArgs
|
& $mixBatPath $newArgs
|
||||||
|
|||||||
+10
-35
@@ -1,16 +1,9 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule EEx.SyntaxError do
|
defmodule EEx.SyntaxError do
|
||||||
defexception [:file, :line, :column, :snippet, message: "syntax error"]
|
defexception [:message, :file, :line, :column]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def message(exception) do
|
def message(exception) do
|
||||||
%{file: file, line: line, column: column, message: message, snippet: snippet} = exception
|
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
|
||||||
|
|
||||||
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column, " ") <>
|
|
||||||
message <> (snippet || "")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,19 +111,6 @@ defmodule EEx do
|
|||||||
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
|
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
|
||||||
| {:eof, metadata}
|
| {:eof, metadata}
|
||||||
|
|
||||||
@type tokenize_opt ::
|
|
||||||
{:file, binary()}
|
|
||||||
| {:line, line}
|
|
||||||
| {:column, column}
|
|
||||||
| {:indentation, non_neg_integer}
|
|
||||||
| {:trim, boolean()}
|
|
||||||
|
|
||||||
@type compile_opt ::
|
|
||||||
tokenize_opt
|
|
||||||
| {:engine, module()}
|
|
||||||
| {:parser_options, Code.parser_opts()}
|
|
||||||
| {atom(), term()}
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates a function definition from the given string.
|
Generates a function definition from the given string.
|
||||||
|
|
||||||
@@ -141,7 +121,6 @@ defmodule EEx do
|
|||||||
template.
|
template.
|
||||||
|
|
||||||
The supported `options` are described [in the module docs](#module-options).
|
The supported `options` are described [in the module docs](#module-options).
|
||||||
Additional options are passed to the underlying engine.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -234,11 +213,9 @@ defmodule EEx do
|
|||||||
"3"
|
"3"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
|
@spec compile_string(String.t(), keyword) :: Macro.t()
|
||||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||||
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
|
case tokenize(source, options) do
|
||||||
|
|
||||||
case tokenize(source, tokenize_opts) do
|
|
||||||
{:ok, tokens} ->
|
{:ok, tokens} ->
|
||||||
EEx.Compiler.compile(tokens, source, options)
|
EEx.Compiler.compile(tokens, source, options)
|
||||||
|
|
||||||
@@ -275,7 +252,7 @@ defmodule EEx do
|
|||||||
#=> "3"
|
#=> "3"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
|
@spec compile_file(Path.t(), keyword) :: Macro.t()
|
||||||
def compile_file(filename, options \\ []) when is_list(options) do
|
def compile_file(filename, options \\ []) when is_list(options) do
|
||||||
filename = IO.chardata_to_string(filename)
|
filename = IO.chardata_to_string(filename)
|
||||||
options = Keyword.merge([file: filename, line: 1], options)
|
options = Keyword.merge([file: filename, line: 1], options)
|
||||||
@@ -293,7 +270,7 @@ defmodule EEx do
|
|||||||
"foo baz"
|
"foo baz"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term()
|
@spec eval_string(String.t(), keyword, keyword) :: String.t()
|
||||||
def eval_string(source, bindings \\ [], options \\ [])
|
def eval_string(source, bindings \\ [], options \\ [])
|
||||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||||
compiled = compile_string(source, options)
|
compiled = compile_string(source, options)
|
||||||
@@ -315,7 +292,7 @@ defmodule EEx do
|
|||||||
#=> "foo baz"
|
#=> "foo baz"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t()
|
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
|
||||||
def eval_file(filename, bindings \\ [], options \\ [])
|
def eval_file(filename, bindings \\ [], options \\ [])
|
||||||
when is_list(bindings) and is_list(options) do
|
when is_list(bindings) and is_list(options) do
|
||||||
filename = IO.chardata_to_string(filename)
|
filename = IO.chardata_to_string(filename)
|
||||||
@@ -337,14 +314,13 @@ defmodule EEx do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> EEx.tokenize(~c"foo", line: 1, column: 1)
|
iex> EEx.tokenize('foo', line: 1, column: 1)
|
||||||
{:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
|
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
|
||||||
|
|
||||||
## Result
|
## Result
|
||||||
|
|
||||||
It returns `{:ok, [token]}` where a token is one of:
|
It returns `{:ok, [token]}` where a token is one of:
|
||||||
|
|
||||||
* `{:comment, content, %{column: column, line: line}}`
|
|
||||||
* `{:text, content, %{column: column, line: line}}`
|
* `{:text, content, %{column: column, line: line}}`
|
||||||
* `{:expr, marker, content, %{column: column, line: line}}`
|
* `{:expr, marker, content, %{column: column, line: line}}`
|
||||||
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
||||||
@@ -356,7 +332,7 @@ defmodule EEx do
|
|||||||
Note new tokens may be added in the future.
|
Note new tokens may be added in the future.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.14.0"
|
@doc since: "1.14.0"
|
||||||
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
|
@spec tokenize([char()] | String.t(), opts :: keyword) ::
|
||||||
{:ok, [token()]} | {:error, String.t(), metadata()}
|
{:ok, [token()]} | {:error, String.t(), metadata()}
|
||||||
def tokenize(contents, opts \\ []) do
|
def tokenize(contents, opts \\ []) do
|
||||||
EEx.Compiler.tokenize(contents, opts)
|
EEx.Compiler.tokenize(contents, opts)
|
||||||
@@ -365,7 +341,6 @@ defmodule EEx do
|
|||||||
### Helpers
|
### Helpers
|
||||||
|
|
||||||
defp do_eval(compiled, bindings, options) do
|
defp do_eval(compiled, bindings, options) do
|
||||||
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
|
|
||||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|||||||
+47
-107
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule EEx.Compiler do
|
defmodule EEx.Compiler do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@@ -52,14 +48,8 @@ defmodule EEx.Compiler do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Remove me on Elixir v2.0
|
# TODO: Deprecate this on Elixir v1.18
|
||||||
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
|
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
|
||||||
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
|
|
||||||
line: line,
|
|
||||||
column: column,
|
|
||||||
file: state.file
|
|
||||||
)
|
|
||||||
|
|
||||||
case expr(t, line, column + 3, state, []) do
|
case expr(t, line, column + 3, state, []) do
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
{:error, message, %{line: line, column: column}}
|
{:error, message, %{line: line, column: column}}
|
||||||
@@ -79,15 +69,15 @@ defmodule EEx.Compiler do
|
|||||||
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
||||||
|
|
||||||
{:ok, expr, new_line, new_column, rest} ->
|
{:ok, expr, new_line, new_column, rest} ->
|
||||||
{key, expr, extra_meta} =
|
{key, expr} =
|
||||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||||
{:ok, _line, _column, _warnings, rev_tokens, []} ->
|
{:ok, _line, _column, _warnings, tokens} ->
|
||||||
# We ignore warnings because the code will be tokenized
|
# We ignore warnings because the code will be tokenized
|
||||||
# again later with the right line+column info
|
# again later with the right line+column info
|
||||||
token_key(rev_tokens, expr)
|
token_key(tokens, expr)
|
||||||
|
|
||||||
{:error, _, _, _, _} ->
|
{:error, _, _, _, _} ->
|
||||||
{:expr, expr, %{}}
|
{:expr, expr}
|
||||||
end
|
end
|
||||||
|
|
||||||
marker =
|
marker =
|
||||||
@@ -96,14 +86,14 @@ defmodule EEx.Compiler do
|
|||||||
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
||||||
"please remove \"#{marker}\""
|
"please remove \"#{marker}\""
|
||||||
|
|
||||||
IO.warn(message, file: state.file, line: line, column: column)
|
:elixir_errors.erl_warn({line, column}, state.file, message)
|
||||||
~c""
|
~c""
|
||||||
else
|
else
|
||||||
marker
|
marker
|
||||||
end
|
end
|
||||||
|
|
||||||
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
|
token = {key, marker, expr, %{line: line, column: column}}
|
||||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
|
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -127,27 +117,6 @@ defmodule EEx.Compiler do
|
|||||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Merge middle expressions separated only by whitespace so the whitespace is
|
|
||||||
# part of the Elixir expression, not a separate EEx body.
|
|
||||||
defp merge_token(
|
|
||||||
{:middle_expr, ~c"", chars, meta},
|
|
||||||
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
|
|
||||||
) do
|
|
||||||
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
|
|
||||||
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
|
|
||||||
else
|
|
||||||
[
|
|
||||||
{:middle_expr, ~c"", chars, meta},
|
|
||||||
{:text, text, text_meta},
|
|
||||||
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp merge_token(token, acc) do
|
|
||||||
[token | acc]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Retrieve marker for <%
|
# Retrieve marker for <%
|
||||||
|
|
||||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||||
@@ -195,40 +164,38 @@ defmodule EEx.Compiler do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Receives tokens and check if it is a start, middle or an end token.
|
# Receives tokens and check if it is a start, middle or an end token.
|
||||||
defp token_key(rev_tokens, expr) do
|
defp token_key(tokens, expr) do
|
||||||
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
|
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
|
||||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||||
{:middle_expr, expr, %{}}
|
{:middle_expr, expr}
|
||||||
|
|
||||||
{_, [{:do, _} | _]} ->
|
{_, [{:do, _} | _]} ->
|
||||||
{:start_expr, maybe_append_space(expr), %{}}
|
{:start_expr, maybe_append_space(expr)}
|
||||||
|
|
||||||
{_, [{:block_identifier, _, identifier} | _]} ->
|
{_, [{:block_identifier, _, _} | _]} ->
|
||||||
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
|
{:middle_expr, maybe_append_space(expr)}
|
||||||
|
|
||||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||||
{:middle_expr, expr, %{}}
|
{:middle_expr, expr}
|
||||||
|
|
||||||
{_, [{:stab_op, _, _} | rev_tokens]} ->
|
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||||
if fn_before_end?(rev_tokens) do
|
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||||
{:start_expr, expr, %{}}
|
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||||
|
|
||||||
|
if end_index > fn_index do
|
||||||
|
{:start_expr, expr}
|
||||||
else
|
else
|
||||||
{:middle_expr, expr, %{}}
|
{:middle_expr, expr}
|
||||||
end
|
end
|
||||||
|
|
||||||
{tokens, _} ->
|
{tokens, _} ->
|
||||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||||
[{:end, _} | _] -> {:end_expr, expr, %{}}
|
[{:end, _} | _] -> {:end_expr, expr}
|
||||||
_ -> {:expr, expr, %{}}
|
_ -> {:expr, expr}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fn_before_end?([{:fn, _} | _]), do: true
|
|
||||||
defp fn_before_end?([{:end, _} | _]), do: false
|
|
||||||
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
|
|
||||||
defp fn_before_end?([]), do: false
|
|
||||||
|
|
||||||
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
||||||
defp drop_eol(rest), do: rest
|
defp drop_eol(rest), do: rest
|
||||||
|
|
||||||
@@ -326,20 +293,12 @@ defmodule EEx.Compiler do
|
|||||||
file: file,
|
file: file,
|
||||||
source: source,
|
source: source,
|
||||||
line: line,
|
line: line,
|
||||||
quoted: %{},
|
quoted: [],
|
||||||
parser_options: [indentation: indentation] ++ parser_options,
|
parser_options: parser_options,
|
||||||
indentation: indentation
|
indentation: indentation
|
||||||
}
|
}
|
||||||
|
|
||||||
init = state.engine.init(opts)
|
init = state.engine.init(opts)
|
||||||
|
|
||||||
if function_exported?(state.engine, :handle_text, 2) and
|
|
||||||
not function_exported?(state.engine, :handle_text, 3) do
|
|
||||||
IO.warn(
|
|
||||||
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
generate_buffer(tokens, init, [], state)
|
generate_buffer(tokens, init, [], state)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -357,7 +316,8 @@ defmodule EEx.Compiler do
|
|||||||
meta = [line: meta.line, column: meta.column]
|
meta = [line: meta.line, column: meta.column]
|
||||||
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
|
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
|
||||||
else
|
else
|
||||||
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
|
# TODO: Deprecate this branch on Elixir v1.18.
|
||||||
|
# We should most likely move this check to init to emit the deprecation once.
|
||||||
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -370,7 +330,7 @@ defmodule EEx.Compiler do
|
|||||||
state.parser_options
|
state.parser_options
|
||||||
|
|
||||||
expr = Code.string_to_quoted!(chars, options)
|
expr = Code.string_to_quoted!(chars, options)
|
||||||
buffer = handle_expr(buffer, mark, expr, meta, state)
|
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||||
generate_buffer(rest, buffer, scope, state)
|
generate_buffer(rest, buffer, scope, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -380,6 +340,13 @@ defmodule EEx.Compiler do
|
|||||||
scope,
|
scope,
|
||||||
state
|
state
|
||||||
) do
|
) do
|
||||||
|
if mark == ~c"" do
|
||||||
|
message =
|
||||||
|
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||||
|
|
||||||
|
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||||
|
end
|
||||||
|
|
||||||
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
||||||
start_line = meta.line
|
start_line = meta.line
|
||||||
start_column = column(meta.column, mark)
|
start_column = column(meta.column, mark)
|
||||||
@@ -389,17 +356,10 @@ defmodule EEx.Compiler do
|
|||||||
rest,
|
rest,
|
||||||
state.engine.handle_begin(buffer),
|
state.engine.handle_begin(buffer),
|
||||||
[{contents, start_line, start_column} | scope],
|
[{contents, start_line, start_column} | scope],
|
||||||
%{state | quoted: %{}, line: line}
|
%{state | quoted: [], line: line}
|
||||||
)
|
)
|
||||||
|
|
||||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||||
message =
|
|
||||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
|
||||||
|
|
||||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
|
||||||
end
|
|
||||||
|
|
||||||
buffer = handle_expr(buffer, mark, contents, meta, state)
|
|
||||||
generate_buffer(rest, buffer, scope, state)
|
generate_buffer(rest, buffer, scope, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -433,7 +393,7 @@ defmodule EEx.Compiler do
|
|||||||
) do
|
) do
|
||||||
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
||||||
options = [file: state.file, line: line, column: column] ++ state.parser_options
|
options = [file: state.file, line: line, column: column] ++ state.parser_options
|
||||||
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
|
tuples = Code.string_to_quoted!(wrapped, options)
|
||||||
buffer = insert_quoted(tuples, state.quoted)
|
buffer = insert_quoted(tuples, state.quoted)
|
||||||
{buffer, rest}
|
{buffer, rest}
|
||||||
end
|
end
|
||||||
@@ -449,7 +409,7 @@ defmodule EEx.Compiler do
|
|||||||
|
|
||||||
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
|
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
|
||||||
message = "expected a closing '<% end %>' for block expression in EEx"
|
message = "expected a closing '<% end %>' for block expression in EEx"
|
||||||
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
|
expr_meta = non_whitespace_meta(content, line, column, state)
|
||||||
syntax_error!(message, expr_meta, state)
|
syntax_error!(message, expr_meta, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -466,10 +426,10 @@ defmodule EEx.Compiler do
|
|||||||
|
|
||||||
defp wrap_expr(current, line, buffer, chars, state) do
|
defp wrap_expr(current, line, buffer, chars, state) do
|
||||||
new_lines = List.duplicate(?\n, line - state.line)
|
new_lines = List.duplicate(?\n, line - state.line)
|
||||||
key = map_size(state.quoted)
|
key = length(state.quoted)
|
||||||
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
|
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
|
||||||
count = [current, placeholder, new_lines, chars]
|
count = current ++ placeholder ++ new_lines ++ chars
|
||||||
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
|
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
|
||||||
|
|
||||||
{count, new_state}
|
{count, new_state}
|
||||||
end
|
end
|
||||||
@@ -499,16 +459,11 @@ defmodule EEx.Compiler do
|
|||||||
Enum.all?(chars, &(&1 in @all_spaces))
|
Enum.all?(chars, &(&1 in @all_spaces))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp clause_block_identifier?(%{block_identifier: identifier}) do
|
|
||||||
identifier in [:else, :rescue, :catch]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp clause_block_identifier?(_meta), do: false
|
|
||||||
|
|
||||||
# Changes placeholder to real expression
|
# Changes placeholder to real expression
|
||||||
|
|
||||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||||
Map.fetch!(quoted, key)
|
{^key, value} = List.keyfind(quoted, key, 0)
|
||||||
|
value
|
||||||
end
|
end
|
||||||
|
|
||||||
defp insert_quoted({left, line, right}, quoted) do
|
defp insert_quoted({left, line, right}, quoted) do
|
||||||
@@ -534,27 +489,12 @@ defmodule EEx.Compiler do
|
|||||||
|
|
||||||
defp syntax_error!(message, meta, state) do
|
defp syntax_error!(message, meta, state) do
|
||||||
raise EEx.SyntaxError,
|
raise EEx.SyntaxError,
|
||||||
message: message,
|
message: message <> code_snippet(state.source, state.indentation, meta),
|
||||||
snippet: code_snippet(state.source, state.indentation, meta),
|
|
||||||
file: state.file,
|
file: state.file,
|
||||||
line: meta.line,
|
line: meta.line,
|
||||||
column: meta.column
|
column: meta.column
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_expr(buffer, mark, expr, meta, state) do
|
|
||||||
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
|
||||||
rescue
|
|
||||||
e in EEx.SyntaxError ->
|
|
||||||
reraise %{
|
|
||||||
e
|
|
||||||
| file: e.file || state.file,
|
|
||||||
line: e.line || meta.line,
|
|
||||||
column: e.column || meta.column,
|
|
||||||
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
|
|
||||||
},
|
|
||||||
__STACKTRACE__
|
|
||||||
end
|
|
||||||
|
|
||||||
defp code_snippet(source, indentation, meta) do
|
defp code_snippet(source, indentation, meta) do
|
||||||
line_start = max(meta.line - 3, 1)
|
line_start = max(meta.line - 3, 1)
|
||||||
line_end = meta.line
|
line_end = meta.line
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule EEx.Engine do
|
defmodule EEx.Engine do
|
||||||
@moduledoc ~S"""
|
@moduledoc ~S"""
|
||||||
Basic EEx engine that ships with Elixir.
|
Basic EEx engine that ships with Elixir.
|
||||||
@@ -17,10 +13,6 @@ defmodule EEx.Engine do
|
|||||||
@doc """
|
@doc """
|
||||||
Called at the beginning of every template.
|
Called at the beginning of every template.
|
||||||
|
|
||||||
It receives the options during compilation, including the
|
|
||||||
ones managed by EEx, such as `:line` and `:file`, as well
|
|
||||||
as custom engine options.
|
|
||||||
|
|
||||||
It must return the initial state.
|
It must return the initial state.
|
||||||
"""
|
"""
|
||||||
@callback init(opts :: keyword) :: state
|
@callback init(opts :: keyword) :: state
|
||||||
@@ -195,7 +187,7 @@ defmodule EEx.Engine do
|
|||||||
def handle_expr(state, "=", ast) do
|
def handle_expr(state, "=", ast) do
|
||||||
check_state!(state)
|
check_state!(state)
|
||||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||||
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__)
|
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||||
|
|
||||||
ast =
|
ast =
|
||||||
quote do
|
quote do
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule EEx.SmartEngine do
|
defmodule EEx.SmartEngine do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The default engine used by EEx.
|
The default engine used by EEx.
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule EEx.MixProject do
|
defmodule EEx.MixProject do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
Code.require_file("../test_helper.exs", __DIR__)
|
Code.require_file("../test_helper.exs", __DIR__)
|
||||||
|
|
||||||
defmodule EEx.SmartEngineTest do
|
defmodule EEx.SmartEngineTest do
|
||||||
@@ -47,6 +43,15 @@ defmodule EEx.SmartEngineTest do
|
|||||||
assert_received :found
|
assert_received :found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||||
|
stderr =
|
||||||
|
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||||
|
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert stderr =~ "the contents of this expression won't be output"
|
||||||
|
end
|
||||||
|
|
||||||
defp assert_eval(expected, actual, binding \\ []) do
|
defp assert_eval(expected, actual, binding \\ []) do
|
||||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
Code.require_file("../test_helper.exs", __DIR__)
|
Code.require_file("../test_helper.exs", __DIR__)
|
||||||
|
|
||||||
defmodule EEx.TokenizerTest do
|
defmodule EEx.TokenizerTest do
|
||||||
@@ -130,21 +126,51 @@ defmodule EEx.TokenizerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "EEx comments" do
|
test "EEx comments" do
|
||||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
exprs = [
|
||||||
exprs = [
|
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
{:eof, %{column: 16, line: 1}}
|
||||||
{:eof, %{column: 16, line: 1}}
|
]
|
||||||
]
|
|
||||||
|
|
||||||
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
|
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
|
||||||
|
|
||||||
exprs = [
|
exprs = [
|
||||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||||
{:eof, %{column: 8, line: 2}}
|
{:eof, %{column: 8, line: 2}}
|
||||||
]
|
]
|
||||||
|
|
||||||
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
|
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
|
||||||
end)
|
end
|
||||||
|
|
||||||
|
test "EEx comments with do-end" do
|
||||||
|
exprs = [
|
||||||
|
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||||
|
{:text, ~c"bar", %{column: 19, line: 1}},
|
||||||
|
{:eof, %{column: 32, line: 1}}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "EEx comments inside do-end" do
|
||||||
|
exprs = [
|
||||||
|
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
|
||||||
|
{:text, ~c"bar", %{column: 31, line: 1}},
|
||||||
|
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
|
||||||
|
{:eof, %{column: 43, line: 1}}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
|
||||||
|
|
||||||
|
exprs = [
|
||||||
|
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
|
||||||
|
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
|
||||||
|
{:text, ~c"bar", %{column: 46, line: 1}},
|
||||||
|
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
|
||||||
|
{:eof, %{column: 58, line: 1}}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
|
||||||
|
{:ok, exprs}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "EEx multi-line comments" do
|
test "EEx multi-line comments" do
|
||||||
@@ -270,7 +296,7 @@ defmodule EEx.TokenizerTest do
|
|||||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||||
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
||||||
{:text, ~c"bar", %{column: 21, line: 1}},
|
{:text, ~c"bar", %{column: 21, line: 1}},
|
||||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
|
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
|
||||||
{:text, ~c"baz", %{column: 34, line: 1}},
|
{:text, ~c"baz", %{column: 34, line: 1}},
|
||||||
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
||||||
{:eof, %{column: 46, line: 1}}
|
{:eof, %{column: 46, line: 1}}
|
||||||
@@ -286,7 +312,7 @@ defmodule EEx.TokenizerTest do
|
|||||||
exprs = [
|
exprs = [
|
||||||
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
||||||
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
||||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
|
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
|
||||||
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
||||||
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
||||||
{:eof, %{column: 3, line: 7}}
|
{:eof, %{column: 3, line: 7}}
|
||||||
@@ -295,6 +321,15 @@ defmodule EEx.TokenizerTest do
|
|||||||
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
|
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "trim mode with comment" do
|
||||||
|
exprs = [
|
||||||
|
{:text, ~c"\n123", %{column: 19, line: 1}},
|
||||||
|
{:eof, %{column: 4, line: 2}}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
|
||||||
|
end
|
||||||
|
|
||||||
test "trim mode with multi-line comment" do
|
test "trim mode with multi-line comment" do
|
||||||
exprs = [
|
exprs = [
|
||||||
{:comment, ~c" comment ", %{column: 3, line: 1}},
|
{:comment, ~c" comment ", %{column: 3, line: 1}},
|
||||||
@@ -349,6 +384,9 @@ defmodule EEx.TokenizerTest do
|
|||||||
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
|
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
|
||||||
{:error, message, %{column: 5, line: 1}}
|
{:error, message, %{column: 5, line: 1}}
|
||||||
|
|
||||||
|
assert EEx.tokenize(~c"<%# true ", @opts) ==
|
||||||
|
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
|
||||||
|
|
||||||
message = """
|
message = """
|
||||||
expected closing '--%>' for EEx expression
|
expected closing '--%>' for EEx expression
|
||||||
|
|
|
|
||||||
|
|||||||
+41
-167
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
Code.require_file("test_helper.exs", __DIR__)
|
Code.require_file("test_helper.exs", __DIR__)
|
||||||
|
|
||||||
require EEx
|
require EEx
|
||||||
@@ -202,6 +198,15 @@ defmodule EExTest do
|
|||||||
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
|
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "embedded code with comments in do end" do
|
||||||
|
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
|
||||||
|
|
||||||
|
assert_eval(
|
||||||
|
"foo\n\nbar\n",
|
||||||
|
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "embedded code with multi-line comments in do end" do
|
test "embedded code with multi-line comments in do end" do
|
||||||
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
|
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
|
||||||
|
|
||||||
@@ -262,69 +267,12 @@ defmodule EExTest do
|
|||||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with expression with else clause split across tags" do
|
|
||||||
template = """
|
|
||||||
<%= with {:ok, x} <- @res do %>
|
|
||||||
<p><%= x %></p>
|
|
||||||
<% else %>
|
|
||||||
<% _ -> %>
|
|
||||||
<p>bad</p>
|
|
||||||
<% end %>
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
|
|
||||||
engine: EEx.SmartEngine
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
|
|
||||||
engine: EEx.SmartEngine
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "empty clauses separated by whitespace" do
|
|
||||||
template = """
|
|
||||||
<%= case x do %>
|
|
||||||
<% :foo -> %>
|
|
||||||
<% :bar -> %>
|
|
||||||
<% end %>
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert_eval("\n \n", template, x: :foo)
|
|
||||||
assert_eval("\n\n", template, x: :bar)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "with end of token" do
|
test "with end of token" do
|
||||||
assert_eval("foo bar %>", "foo bar %>")
|
assert_eval("foo bar %>", "foo bar %>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "raises syntax errors" do
|
describe "raises syntax errors" do
|
||||||
test "with relative file information" do
|
|
||||||
message = """
|
|
||||||
foobar.eex:1:5: expected closing '%>' for EEx expression
|
|
||||||
|
|
|
||||||
1 | foo <%= bar
|
|
||||||
| ^\
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert_raise EEx.SyntaxError, message, fn ->
|
|
||||||
EEx.compile_string("foo <%= bar", file: Path.join(File.cwd!(), "foobar.eex"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "when <%!-- is not closed" do
|
|
||||||
message = """
|
|
||||||
my_file.eex:1:5: expected closing '--%>' for EEx expression
|
|
||||||
|
|
|
||||||
1 | foo <%!-- bar
|
|
||||||
| ^\
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert_raise EEx.SyntaxError, message, fn ->
|
|
||||||
EEx.compile_string("foo <%!-- bar", file: "my_file.eex")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "when the token is invalid" do
|
test "when the token is invalid" do
|
||||||
message = """
|
message = """
|
||||||
nofile:1:5: expected closing '%>' for EEx expression
|
nofile:1:5: expected closing '%>' for EEx expression
|
||||||
@@ -533,59 +481,6 @@ defmodule EExTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "from Elixir parser" do
|
|
||||||
line = __ENV__.line + 6
|
|
||||||
|
|
||||||
message =
|
|
||||||
assert_raise TokenMissingError, fn ->
|
|
||||||
EEx.compile_string(
|
|
||||||
"""
|
|
||||||
<li>
|
|
||||||
<strong>Some:</strong>
|
|
||||||
<%= true && @some[ %>
|
|
||||||
</li>
|
|
||||||
""",
|
|
||||||
file: __ENV__.file,
|
|
||||||
line: line,
|
|
||||||
indentation: 12
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
|
||||||
│
|
|
||||||
#{line + 2} │ true && @some[\s
|
|
||||||
│ │ └ missing closing delimiter (expected "]")
|
|
||||||
│ └ unclosed delimiter
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "from Elixir parser with line breaks" do
|
|
||||||
line = __ENV__.line + 6
|
|
||||||
|
|
||||||
message =
|
|
||||||
assert_raise TokenMissingError, fn ->
|
|
||||||
EEx.compile_string(
|
|
||||||
"""
|
|
||||||
<li>
|
|
||||||
<strong>Some:</strong>
|
|
||||||
<%= true &&
|
|
||||||
@some[ %>
|
|
||||||
</li>
|
|
||||||
""",
|
|
||||||
file: __ENV__.file,
|
|
||||||
line: line,
|
|
||||||
indentation: 12
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
|
||||||
│
|
|
||||||
#{line + 3} │ @some[\s
|
|
||||||
│ │ └ missing closing delimiter (expected "]")
|
|
||||||
│ └ unclosed delimiter
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "honor line numbers" do
|
test "honor line numbers" do
|
||||||
assert_raise EEx.SyntaxError,
|
assert_raise EEx.SyntaxError,
|
||||||
"nofile:100:6: expected closing '%>' for EEx expression",
|
"nofile:100:6: expected closing '%>' for EEx expression",
|
||||||
@@ -606,18 +501,6 @@ defmodule EExTest do
|
|||||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "unsupported marker error carries template location metadata" do
|
|
||||||
error =
|
|
||||||
assert_raise EEx.SyntaxError, fn ->
|
|
||||||
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
|
|
||||||
end
|
|
||||||
|
|
||||||
assert error.file == "sample.eex"
|
|
||||||
assert error.line == 7
|
|
||||||
assert error.column == 1
|
|
||||||
assert Exception.message(error) =~ "sample.eex:7:1:"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "warnings" do
|
describe "warnings" do
|
||||||
@@ -634,16 +517,6 @@ defmodule EExTest do
|
|||||||
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "unused \"do\" block without \"<%=\" modifier" do
|
|
||||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
||||||
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
|
|
||||||
end) =~ "the contents of this expression won't be output"
|
|
||||||
|
|
||||||
# These are fine though
|
|
||||||
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
|
|
||||||
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "from tokenizer" do
|
test "from tokenizer" do
|
||||||
warning =
|
warning =
|
||||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||||
@@ -862,31 +735,38 @@ defmodule EExTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "line and column meta" do
|
test "line and column meta" do
|
||||||
indentation = 12
|
parser_options = Code.get_compiler_option(:parser_options)
|
||||||
|
Code.put_compiler_option(:parser_options, columns: true)
|
||||||
|
|
||||||
ast =
|
try do
|
||||||
EEx.compile_string(
|
indentation = 12
|
||||||
"""
|
|
||||||
<%= f() %> <% f() %>
|
|
||||||
<%= f fn -> %>
|
|
||||||
<%= f() %>
|
|
||||||
<% end %>
|
|
||||||
""",
|
|
||||||
indentation: indentation
|
|
||||||
)
|
|
||||||
|
|
||||||
{_, calls} =
|
ast =
|
||||||
Macro.prewalk(ast, [], fn
|
EEx.compile_string(
|
||||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
"""
|
||||||
other, acc -> {other, acc}
|
<%= f() %> <% f() %>
|
||||||
end)
|
<%= f fn -> %>
|
||||||
|
<%= f() %>
|
||||||
|
<% end %>
|
||||||
|
""",
|
||||||
|
indentation: indentation
|
||||||
|
)
|
||||||
|
|
||||||
assert Enum.reverse(calls) == [
|
{_, calls} =
|
||||||
[line: 1, column: indentation + 5],
|
Macro.prewalk(ast, [], fn
|
||||||
[line: 1, column: indentation + 15],
|
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||||
[line: 2, column: indentation + 7],
|
other, acc -> {other, acc}
|
||||||
[line: 3, column: indentation + 9]
|
end)
|
||||||
]
|
|
||||||
|
assert Enum.reverse(calls) == [
|
||||||
|
[line: 1, column: indentation + 5],
|
||||||
|
[line: 1, column: indentation + 15],
|
||||||
|
[line: 2, column: indentation + 7],
|
||||||
|
[line: 3, column: indentation + 9]
|
||||||
|
]
|
||||||
|
after
|
||||||
|
Code.put_compiler_option(:parser_options, parser_options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -960,13 +840,13 @@ defmodule EExTest do
|
|||||||
file = to_charlist(Path.relative_to_cwd(__ENV__.file))
|
file = to_charlist(Path.relative_to_cwd(__ENV__.file))
|
||||||
|
|
||||||
assert EExTest.Compiled.before_compile() ==
|
assert EExTest.Compiled.before_compile() ==
|
||||||
{11, {EExTest.Compiled, :before_compile, 0, [file: file, line: 11]}}
|
{7, {EExTest.Compiled, :before_compile, 0, [file: file, line: 7]}}
|
||||||
|
|
||||||
assert EExTest.Compiled.after_compile() ==
|
assert EExTest.Compiled.after_compile() ==
|
||||||
{25, {EExTest.Compiled, :after_compile, 0, [file: file, line: 25]}}
|
{21, {EExTest.Compiled, :after_compile, 0, [file: file, line: 21]}}
|
||||||
|
|
||||||
assert EExTest.Compiled.unknown() ==
|
assert EExTest.Compiled.unknown() ==
|
||||||
{30, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 30]}}
|
{26, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 26]}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1044,12 +924,6 @@ defmodule EExTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@strip_ansi [IO.ANSI.green(), IO.ANSI.red(), IO.ANSI.reset()]
|
|
||||||
|
|
||||||
defp strip_ansi(doc) do
|
|
||||||
String.replace(doc, @strip_ansi, "")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
|
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
|
||||||
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
|
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
|
||||||
result = EEx.eval_string(actual, binding, opts)
|
result = EEx.eval_string(actual, binding, opts)
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
{line_exclude, line_include} =
|
{line_exclude, line_include} =
|
||||||
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
|
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
|
||||||
|
|
||||||
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
|
ExUnit.start(
|
||||||
CoverageRecorder.maybe_record("eex")
|
trace: !!System.get_env("TRACE"),
|
||||||
|
include: line_include,
|
||||||
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: []
|
exclude: line_exclude
|
||||||
|
)
|
||||||
ex_unit_opts =
|
|
||||||
[
|
|
||||||
trace: !!System.get_env("TRACE"),
|
|
||||||
include: line_include,
|
|
||||||
exclude: line_exclude
|
|
||||||
] ++ maybe_seed_opt
|
|
||||||
|
|
||||||
ExUnit.start(ex_unit_opts)
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
%% SPDX-License-Identifier: Apache-2.0
|
|
||||||
%% SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
%% SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
{'src/*', [
|
{'src/*', [
|
||||||
warn_unused_vars,
|
warn_unused_vars,
|
||||||
warn_export_all,
|
warn_export_all,
|
||||||
@@ -13,8 +9,8 @@
|
|||||||
warn_deprecated_function,
|
warn_deprecated_function,
|
||||||
warn_obsolete_guard,
|
warn_obsolete_guard,
|
||||||
warn_exported_vars,
|
warn_exported_vars,
|
||||||
%% Enable this when we require Erlang/OTP 27+
|
%% warn_missing_spec,
|
||||||
%% warnings_as_errors,
|
%% warn_untyped_record,
|
||||||
debug_info,
|
debug_info,
|
||||||
{outdir, "ebin/"}
|
{outdir, "ebin/"}
|
||||||
]}.
|
]}.
|
||||||
|
|||||||
+74
-308
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Access do
|
defmodule Access do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Key-based access to data structures.
|
Key-based access to data structures.
|
||||||
@@ -10,8 +6,8 @@ defmodule Access do
|
|||||||
keys of any type in a data structure via the `data[key]` syntax.
|
keys of any type in a data structure via the `data[key]` syntax.
|
||||||
|
|
||||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||||
of the box. Keyword lists support only atom keys, while keys for maps
|
of the box. Keywords supports only atoms keys, keys for maps can
|
||||||
can be of any type. Both return `nil` if the key does not exist:
|
be of any type. Both return `nil` if the key does not exist:
|
||||||
|
|
||||||
iex> keywords = [a: 1, b: 2]
|
iex> keywords = [a: 1, b: 2]
|
||||||
iex> keywords[:a]
|
iex> keywords[:a]
|
||||||
@@ -33,80 +29,67 @@ defmodule Access do
|
|||||||
iex> keywords[:c][:unknown]
|
iex> keywords[:c][:unknown]
|
||||||
nil
|
nil
|
||||||
|
|
||||||
This works because accessing anything on a `nil` value returns
|
This works because accessing anything on a `nil` value, returns
|
||||||
`nil` itself:
|
`nil` itself:
|
||||||
|
|
||||||
iex> nil[:a]
|
iex> nil[:a]
|
||||||
nil
|
nil
|
||||||
|
|
||||||
|
The access syntax can also be used with the `Kernel.put_in/2`,
|
||||||
|
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
|
||||||
|
to allow values to be set in nested data structures:
|
||||||
|
|
||||||
|
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||||
|
iex> put_in(users["john"][:age], 28)
|
||||||
|
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
||||||
|
|
||||||
## Maps and structs
|
## Maps and structs
|
||||||
|
|
||||||
While the access syntax is allowed in maps via `map[key]`,
|
While the access syntax is allowed in maps via `map[key]`,
|
||||||
if your map is made of predefined atom keys, you should prefer
|
if your map is made of predefined atom keys, you should prefer
|
||||||
to access those atom keys with `map.key` instead of `map[key]`,
|
to access those atom keys with `map.key` instead of `map[key]`,
|
||||||
as `map.key` will raise if the key is missing (which is not
|
as `map.key` will raise if the key is missing (which is not
|
||||||
supposed to happen if the keys are predefined) or if `map` is
|
supposed to happen if the keys are predefined).
|
||||||
`nil`.
|
|
||||||
|
|
||||||
Similarly, since structs are maps and structs have predefined
|
Similarly, since structs are maps and structs have predefined
|
||||||
keys, they only allow the `struct.key` syntax and they do not
|
keys, they only allow the `struct.key` syntax and they do not
|
||||||
allow the `struct[key]` access syntax.
|
allow the `struct[key]` access syntax. `Access.key/1` can also
|
||||||
|
be used to construct dynamic access to structs and maps.
|
||||||
|
|
||||||
In other words, the `map[key]` syntax is loose, returning `nil`
|
In a nutshell, when using `put_in/2` and friends:
|
||||||
for missing keys, while the `map.key` syntax is strict, raising
|
|
||||||
for both nil values and missing keys.
|
|
||||||
|
|
||||||
To bridge this gap, Elixir provides the `get_in/1` and `get_in/2`
|
put_in(struct_or_map.key, :value)
|
||||||
functions, which are capable of traversing nested data structures,
|
put_in(keyword_or_map[:key], :value)
|
||||||
even in the presence of `nil`s:
|
|
||||||
|
|
||||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
When using `put_in/3` and friends:
|
||||||
iex> get_in(users["john"].age)
|
|
||||||
27
|
|
||||||
iex> get_in(users["unknown"].age)
|
|
||||||
nil
|
|
||||||
|
|
||||||
Notice how, even if no user was found, `get_in/1` returned `nil`.
|
put_in(struct_or_map, [Access.key!(:key)], :value)
|
||||||
Outside of `get_in/1`, trying to access the field `.age` on `nil`
|
put_in(keyword_or_map, [:key], :value)
|
||||||
would raise.
|
|
||||||
|
|
||||||
The `get_in/2` function takes one step further by allowing
|
This covers the dual nature of maps in Elixir, as they can be
|
||||||
different accessors to be mixed in. For example, given a user
|
either for structured data or as a key-value store. See the `Map`
|
||||||
map with the `:name` and `:languages` keys, here is how to
|
module for more information.
|
||||||
access the name of all programming languages:
|
|
||||||
|
|
||||||
iex> languages = [
|
## Nested data structures
|
||||||
...> %{name: "elixir", type: :functional},
|
|
||||||
...> %{name: "c", type: :procedural}
|
|
||||||
...> ]
|
|
||||||
iex> user = %{name: "john", languages: languages}
|
|
||||||
iex> get_in(user, [:languages, Access.all(), :name])
|
|
||||||
["elixir", "c"]
|
|
||||||
|
|
||||||
This module provides convenience functions for traversing other
|
Both key-based access syntaxes can be used with the nested update
|
||||||
structures, like tuples and lists. As we will see next, they can
|
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
|
||||||
even be used to update nested data structures.
|
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
|
||||||
|
`Kernel.get_and_update_in/3`.
|
||||||
|
|
||||||
If you want to learn more about the dual nature of maps in Elixir,
|
For example, to update a map inside another map:
|
||||||
as they can be either for structured data or as a key-value store,
|
|
||||||
see the `Map` module.
|
|
||||||
|
|
||||||
## Updating nested data structures
|
|
||||||
|
|
||||||
The access syntax can also be used with the `Kernel.put_in/2`,
|
|
||||||
`Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1`
|
|
||||||
macros to further manipulate values in nested data structures:
|
|
||||||
|
|
||||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||||
iex> put_in(users["john"].age, 28)
|
iex> put_in(users["john"].age, 28)
|
||||||
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
||||||
|
|
||||||
As shown in the previous section, you can also use the
|
This module provides convenience functions for traversing other
|
||||||
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
|
structures, like tuples and lists. These functions can be used
|
||||||
`Kernel.get_and_update_in/3` functions to provide nested
|
in all the `Access`-related functions and macros in `Kernel`.
|
||||||
custom accessors. For instance, given a user map with the
|
|
||||||
`:name` and `:languages` keys, here is how to deeply traverse
|
For instance, given a user map with the `:name` and `:languages`
|
||||||
the map and convert all language names to uppercase:
|
keys, here is how to deeply traverse the map and convert all
|
||||||
|
language names to uppercase:
|
||||||
|
|
||||||
iex> languages = [
|
iex> languages = [
|
||||||
...> %{name: "elixir", type: :functional},
|
...> %{name: "elixir", type: :functional},
|
||||||
@@ -226,8 +209,6 @@ defmodule Access do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches the value for the given key in a container (a map, keyword
|
Fetches the value for the given key in a container (a map, keyword
|
||||||
list, or struct that implements the `Access` behaviour).
|
list, or struct that implements the `Access` behaviour).
|
||||||
@@ -356,7 +337,7 @@ defmodule Access do
|
|||||||
Accessing a list by index is typically discouraged in Elixir, \
|
Accessing a list by index is typically discouraged in Elixir, \
|
||||||
instead we prefer to use the Enum module to manipulate lists \
|
instead we prefer to use the Enum module to manipulate lists \
|
||||||
as a whole. If you really must access a list element by index, \
|
as a whole. If you really must access a list element by index, \
|
||||||
you can use Enum.at/2 or the functions in the List module\
|
you can Enum.at/1 or the functions in the List module\
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -424,8 +405,8 @@ defmodule Access do
|
|||||||
|
|
||||||
Accessing a list by index is typically discouraged in Elixir, \
|
Accessing a list by index is typically discouraged in Elixir, \
|
||||||
instead we prefer to use the Enum module to manipulate lists \
|
instead we prefer to use the Enum module to manipulate lists \
|
||||||
as a whole. If you really must modify a list element by index, \
|
as a whole. If you really must mostify a list element by index, \
|
||||||
you can use Access.at/1 or the functions in the List module\
|
you can Access.at/1 or the functions in the List module\
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -487,7 +468,7 @@ defmodule Access do
|
|||||||
## Accessors
|
## Accessors
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
Returns a function that accesses the given key in a map/struct.
|
||||||
|
|
||||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||||
`Kernel.get_and_update_in/3`, and friends.
|
`Kernel.get_and_update_in/3`, and friends.
|
||||||
@@ -516,56 +497,30 @@ defmodule Access do
|
|||||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||||
{"john", %{user: %{}}}
|
{"john", %{user: %{}}}
|
||||||
|
|
||||||
iex> keyword = [user: [name: "john"]]
|
An error is raised if the accessed structure is not a map or a struct:
|
||||||
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
|
|
||||||
"john"
|
|
||||||
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
|
|
||||||
...> {prev, String.upcase(prev)}
|
|
||||||
...> end)
|
|
||||||
{"john", [user: [name: "JOHN"]]}
|
|
||||||
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
|
|
||||||
{"john", [user: []]}
|
|
||||||
|
|
||||||
An error is raised if the accessed structure is not a map, struct, or keyword list:
|
iex> get_in([], [Access.key(:foo)])
|
||||||
|
** (BadMapError) expected a map, got: []
|
||||||
|
|
||||||
iex> get_in(123, [Access.key(:foo)])
|
|
||||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
|
||||||
|
|
||||||
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
|
||||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
|
||||||
"""
|
"""
|
||||||
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||||
def key(key, default \\ nil) do
|
def key(key, default \\ nil) do
|
||||||
fn
|
fn
|
||||||
:get, %{} = data, next ->
|
:get, data, next ->
|
||||||
next.(Map.get(data, key, default))
|
next.(Map.get(data, key, default))
|
||||||
|
|
||||||
:get_and_update, %{} = data, next ->
|
:get_and_update, data, next ->
|
||||||
value = Map.get(data, key, default)
|
value = Map.get(data, key, default)
|
||||||
|
|
||||||
case next.(value) do
|
case next.(value) do
|
||||||
{get, update} -> {get, Map.put(data, key, update)}
|
{get, update} -> {get, Map.put(data, key, update)}
|
||||||
:pop -> {value, Map.delete(data, key)}
|
:pop -> {value, Map.delete(data, key)}
|
||||||
end
|
end
|
||||||
|
|
||||||
:get, data, next when is_probably_keyword(data) ->
|
|
||||||
next.(Keyword.get(data, key, default))
|
|
||||||
|
|
||||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
|
||||||
value = Keyword.get(data, key, default)
|
|
||||||
|
|
||||||
case next.(value) do
|
|
||||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
|
||||||
:pop -> {value, Keyword.delete(data, key)}
|
|
||||||
end
|
|
||||||
|
|
||||||
_op, data, _next ->
|
|
||||||
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
Returns a function that accesses the given key in a map/struct.
|
||||||
|
|
||||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||||
`Kernel.get_and_update_in/3`, and friends.
|
`Kernel.get_and_update_in/3`, and friends.
|
||||||
@@ -574,19 +529,6 @@ defmodule Access do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> keyword = [user: [name: "john"]]
|
|
||||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
|
||||||
"john"
|
|
||||||
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
|
||||||
...> {prev, String.upcase(prev)}
|
|
||||||
...> end)
|
|
||||||
{"john", [user: [name: "JOHN"]]}
|
|
||||||
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
|
||||||
{"john", [user: []]}
|
|
||||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
|
|
||||||
** (KeyError) key :unknown not found in:
|
|
||||||
...
|
|
||||||
|
|
||||||
iex> map = %{user: %{name: "john"}}
|
iex> map = %{user: %{name: "john"}}
|
||||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||||
"john"
|
"john"
|
||||||
@@ -597,8 +539,7 @@ defmodule Access do
|
|||||||
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
|
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||||
{"john", %{user: %{}}}
|
{"john", %{user: %{}}}
|
||||||
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
|
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
|
||||||
** (KeyError) key :unknown not found in:
|
** (KeyError) key :unknown not found in: %{name: \"john\"}
|
||||||
...
|
|
||||||
|
|
||||||
The examples above could be partially written as:
|
The examples above could be partially written as:
|
||||||
|
|
||||||
@@ -611,19 +552,17 @@ defmodule Access do
|
|||||||
{"john", %{user: %{name: "JOHN"}}}
|
{"john", %{user: %{name: "JOHN"}}}
|
||||||
|
|
||||||
However, it is not possible to remove fields using the dot notation,
|
However, it is not possible to remove fields using the dot notation,
|
||||||
as it is implied those fields must also be present. In any case,
|
as it is implified those fields must also be present. In any case,
|
||||||
`Access.key!/1` is useful when the key is not known in advance
|
`Access.key!/1` is useful when the key is not known in advance
|
||||||
and must be accessed dynamically.
|
and must be accessed dynamically.
|
||||||
|
|
||||||
An error is raised if the accessed structure is not a map/struct/keyword list:
|
An error is raised if the accessed structure is not a map/struct:
|
||||||
|
|
||||||
iex> get_in(123, [Access.key!(:foo)])
|
iex> get_in([], [Access.key!(:foo)])
|
||||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
|
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||||
|
|
||||||
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
|
||||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
|
|
||||||
"""
|
"""
|
||||||
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||||
def key!(key) do
|
def key!(key) do
|
||||||
fn
|
fn
|
||||||
:get, %{} = data, next ->
|
:get, %{} = data, next ->
|
||||||
@@ -637,19 +576,8 @@ defmodule Access do
|
|||||||
:pop -> {value, Map.delete(data, key)}
|
:pop -> {value, Map.delete(data, key)}
|
||||||
end
|
end
|
||||||
|
|
||||||
:get, data, next when is_probably_keyword(data) ->
|
|
||||||
next.(Keyword.fetch!(data, key))
|
|
||||||
|
|
||||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
|
||||||
value = Keyword.fetch!(data, key)
|
|
||||||
|
|
||||||
case next.(value) do
|
|
||||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
|
||||||
:pop -> {value, Keyword.delete(data, key)}
|
|
||||||
end
|
|
||||||
|
|
||||||
_op, data, _next ->
|
_op, data, _next ->
|
||||||
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
|
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -866,7 +794,7 @@ defmodule Access do
|
|||||||
iex> get_in([:a, :b, :c], [Access.at!(2)])
|
iex> get_in([:a, :b, :c], [Access.at!(2)])
|
||||||
:c
|
:c
|
||||||
iex> get_in([:a, :b, :c], [Access.at!(3)])
|
iex> get_in([:a, :b, :c], [Access.at!(3)])
|
||||||
** (Enum.OutOfBoundsError) out of bounds error at position 3 when traversing enumerable [:a, :b, :c]
|
** (Enum.OutOfBoundsError) out of bounds error
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@@ -878,14 +806,12 @@ defmodule Access do
|
|||||||
defp at!(:get, data, index, next) when is_list(data) do
|
defp at!(:get, data, index, next) when is_list(data) do
|
||||||
case Enum.fetch(data, index) do
|
case Enum.fetch(data, index) do
|
||||||
{:ok, value} -> next.(value)
|
{:ok, value} -> next.(value)
|
||||||
:error -> raise Enum.OutOfBoundsError, index: index, enumerable: data
|
:error -> raise Enum.OutOfBoundsError
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp at!(:get_and_update, data, index, next) when is_list(data) do
|
defp at!(:get_and_update, data, index, next) when is_list(data) do
|
||||||
get_and_update_at(data, index, next, [], fn ->
|
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
|
||||||
raise Enum.OutOfBoundsError, index: index, enumerable: data
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp at!(_op, data, _index, _next) do
|
defp at!(_op, data, _index, _next) do
|
||||||
@@ -917,7 +843,7 @@ defmodule Access do
|
|||||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
||||||
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
||||||
|
|
||||||
When no match is found, an empty list is returned and the update function is never called:
|
When no match is found, an empty list is returned and the update function is never called
|
||||||
|
|
||||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||||
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
||||||
@@ -927,6 +853,11 @@ defmodule Access do
|
|||||||
...> end)
|
...> end)
|
||||||
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||||
|
|
||||||
|
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||||
|
|
||||||
|
iex> get_in([], [Access.filter(5)])
|
||||||
|
** (FunctionClauseError) no function clause matching in Access.filter/1
|
||||||
|
|
||||||
An error is raised if the accessed structure is not a list:
|
An error is raised if the accessed structure is not a list:
|
||||||
|
|
||||||
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
||||||
@@ -934,13 +865,13 @@ defmodule Access do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.6.0"
|
@doc since: "1.6.0"
|
||||||
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list)
|
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||||
def filter(func) when is_function(func) do
|
def filter(func) when is_function(func) do
|
||||||
fn op, data, next -> filter(op, data, func, next) end
|
fn op, data, next -> filter(op, data, func, next) end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter(:get, data, func, next) when is_list(data) do
|
defp filter(:get, data, func, next) when is_list(data) do
|
||||||
for elem <- data, func.(elem), do: next.(elem)
|
data |> Enum.filter(func) |> Enum.map(next)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
||||||
@@ -1035,12 +966,12 @@ defmodule Access do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
||||||
%Range{first: first, last: last, step: step} = normalize_range(range, data)
|
range = normalize_range(range, data)
|
||||||
|
|
||||||
if first > last do
|
if range.first > range.last do
|
||||||
{[], data}
|
{[], data}
|
||||||
else
|
else
|
||||||
get_and_update_slice(data, first, last, step, next, [], [], 0)
|
get_and_update_slice(data, range, next, [], [], 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1048,93 +979,6 @@ defmodule Access do
|
|||||||
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
|
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns a function that accesses all values in a map or a keyword list.
|
|
||||||
|
|
||||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
||||||
`Kernel.get_and_update_in/3`, and friends.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
|
||||||
iex> get_in(users, [Access.values(), :age]) |> Enum.sort()
|
|
||||||
[23, 27]
|
|
||||||
iex> update_in(users, [Access.values(), :age], fn age -> age + 1 end)
|
|
||||||
%{"john" => %{age: 28}, "meg" => %{age: 24}}
|
|
||||||
iex> put_in(users, [Access.values(), :planet], "Earth")
|
|
||||||
%{"john" => %{age: 27, planet: "Earth"}, "meg" => %{age: 23, planet: "Earth"}}
|
|
||||||
|
|
||||||
Values in keyword lists can be accessed as well:
|
|
||||||
|
|
||||||
iex> users = [john: %{age: 27}, meg: %{age: 23}]
|
|
||||||
iex> get_and_update_in(users, [Access.values(), :age], fn age -> {age, age + 1} end)
|
|
||||||
{[27, 23], [john: %{age: 28}, meg: %{age: 24}]}
|
|
||||||
|
|
||||||
By returning `:pop` from an accessor function, you can remove the accessed key and value
|
|
||||||
from the map or keyword list:
|
|
||||||
|
|
||||||
iex> require Integer
|
|
||||||
iex> numbers = [one: 1, two: 2, three: 3, four: 4]
|
|
||||||
iex> get_and_update_in(numbers, [Access.values()], fn num ->
|
|
||||||
...> if Integer.is_even(num), do: :pop, else: {num, to_string(num)}
|
|
||||||
...> end)
|
|
||||||
{[1, 2, 3, 4], [one: "1", three: "3"]}
|
|
||||||
|
|
||||||
An error is raised if the accessed structure is not a map nor a keyword list:
|
|
||||||
|
|
||||||
iex> get_in([1, 2, 3], [Access.values()])
|
|
||||||
** (RuntimeError) Access.values/0 expected a map or a keyword list, got: [1, 2, 3]
|
|
||||||
"""
|
|
||||||
@doc since: "1.19.0"
|
|
||||||
@spec values() :: Access.access_fun(data :: map() | keyword(), current_value :: list())
|
|
||||||
def values do
|
|
||||||
&values/3
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values(:get, data = %{}, next) do
|
|
||||||
Enum.map(data, fn {_key, value} -> next.(value) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values(:get_and_update, data = %{}, next) do
|
|
||||||
{reverse_gets, updated_data} =
|
|
||||||
Enum.reduce(data, {[], %{}}, fn {key, value}, {gets, data_acc} ->
|
|
||||||
case next.(value) do
|
|
||||||
{get, update} -> {[get | gets], Map.put(data_acc, key, update)}
|
|
||||||
:pop -> {[value | gets], data_acc}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
{Enum.reverse(reverse_gets), updated_data}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values(op, data = [], next) do
|
|
||||||
values_keyword(op, data, next)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values(op, data = [{key, _value} | _tail], next) when is_atom(key) do
|
|
||||||
values_keyword(op, data, next)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values(_op, data, _next) do
|
|
||||||
raise "Access.values/0 expected a map or a keyword list, got: #{inspect(data)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values_keyword(:get, data, next) do
|
|
||||||
Enum.map(data, fn {key, value} when is_atom(key) -> next.(value) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp values_keyword(:get_and_update, data, next) do
|
|
||||||
{reverse_gets, reverse_updated_data} =
|
|
||||||
Enum.reduce(data, {[], []}, fn {key, value}, {gets, data_acc} when is_atom(key) ->
|
|
||||||
case next.(value) do
|
|
||||||
{get, update} -> {[get | gets], [{key, update} | data_acc]}
|
|
||||||
:pop -> {[value | gets], data_acc}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
{Enum.reverse(reverse_gets), Enum.reverse(reverse_updated_data)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_range(%Range{first: first, last: last, step: step}, list)
|
defp normalize_range(%Range{first: first, last: last, step: step}, list)
|
||||||
when first < 0 or last < 0 do
|
when first < 0 or last < 0 do
|
||||||
count = length(list)
|
count = length(list)
|
||||||
@@ -1145,23 +989,16 @@ defmodule Access do
|
|||||||
|
|
||||||
defp normalize_range(range, _list), do: range
|
defp normalize_range(range, _list), do: range
|
||||||
|
|
||||||
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index)
|
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||||
when index > last do
|
if index in range do
|
||||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
|
|
||||||
if index >= first and rem(index - first, step) == 0 do
|
|
||||||
case next.(head) do
|
case next.(head) do
|
||||||
:pop ->
|
:pop ->
|
||||||
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1)
|
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
||||||
|
|
||||||
{get, update} ->
|
{get, update} ->
|
||||||
get_and_update_slice(
|
get_and_update_slice(
|
||||||
rest,
|
rest,
|
||||||
first,
|
range,
|
||||||
last,
|
|
||||||
step,
|
|
||||||
next,
|
next,
|
||||||
[update | updates],
|
[update | updates],
|
||||||
[get | gets],
|
[get | gets],
|
||||||
@@ -1169,82 +1006,11 @@ defmodule Access do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1)
|
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do
|
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
||||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Returns a function that accesses the first element of a list that matches the provided predicate.
|
|
||||||
|
|
||||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
||||||
`Kernel.get_and_update_in/3`, and friends.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
||||||
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
|
|
||||||
"francine"
|
|
||||||
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
|
|
||||||
...> {prev, String.upcase(prev)}
|
|
||||||
...> end)
|
|
||||||
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
|
|
||||||
|
|
||||||
`find/1` can also be used to pop the first found element out of a list or
|
|
||||||
a key inside of a list:
|
|
||||||
|
|
||||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
||||||
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
|
|
||||||
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
|
|
||||||
|
|
||||||
When no match is found, nil is returned and the update function is never called:
|
|
||||||
|
|
||||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
||||||
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
|
|
||||||
nil
|
|
||||||
iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn prev ->
|
|
||||||
...> {prev, String.upcase(prev)}
|
|
||||||
...> end)
|
|
||||||
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
|
||||||
|
|
||||||
An error is raised if the accessed structure is not a list:
|
|
||||||
|
|
||||||
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
|
|
||||||
** (RuntimeError) Access.find/1 expected a list, got: %{}
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@spec find((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: term)
|
|
||||||
def find(predicate) when is_function(predicate, 1) do
|
|
||||||
fn op, data, next -> find(op, data, predicate, next) end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find(:get, data, predicate, next) when is_list(data) do
|
|
||||||
data |> Enum.find(predicate) |> next.()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find(:get_and_update, data, predicate, next) when is_list(data) do
|
|
||||||
get_and_update_find(data, [], predicate, next)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find(_op, data, _predicate, _next) do
|
|
||||||
raise "Access.find/1 expected a list, got: #{inspect(data)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_and_update_find([], updates, _predicate, _next) do
|
|
||||||
{nil, :lists.reverse(updates)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_and_update_find([head | rest], updates, predicate, next) do
|
|
||||||
if predicate.(head) do
|
|
||||||
case next.(head) do
|
|
||||||
{get, update} -> {get, :lists.reverse([update | updates], rest)}
|
|
||||||
:pop -> {head, :lists.reverse(updates, rest)}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
get_and_update_find(rest, [head | updates], predicate, next)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
+8
-12
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Agent do
|
defmodule Agent do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Agents are a simple abstraction around state.
|
Agents are a simple abstraction around state.
|
||||||
@@ -211,7 +207,7 @@ defmodule Agent do
|
|||||||
@doc false
|
@doc false
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
quote location: :keep, bind_quoted: [opts: opts] do
|
quote location: :keep, bind_quoted: [opts: opts] do
|
||||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a specification to start this module under a supervisor.
|
Returns a specification to start this module under a supervisor.
|
||||||
|
|
||||||
@@ -288,7 +284,7 @@ defmodule Agent do
|
|||||||
instead of an anonymous function; `fun` in `module` will be called with the
|
instead of an anonymous function; `fun` in `module` will be called with the
|
||||||
given arguments `args` to initialize the state.
|
given arguments `args` to initialize the state.
|
||||||
"""
|
"""
|
||||||
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
|
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
|
||||||
def start_link(module, fun, args, options \\ []) do
|
def start_link(module, fun, args, options \\ []) do
|
||||||
GenServer.start_link(Agent.Server, {module, fun, args}, options)
|
GenServer.start_link(Agent.Server, {module, fun, args}, options)
|
||||||
end
|
end
|
||||||
@@ -315,7 +311,7 @@ defmodule Agent do
|
|||||||
|
|
||||||
See `start_link/4` for more information.
|
See `start_link/4` for more information.
|
||||||
"""
|
"""
|
||||||
@spec start(module, atom, [term], GenServer.options()) :: on_start
|
@spec start(module, atom, [any], GenServer.options()) :: on_start
|
||||||
def start(module, fun, args, options \\ []) do
|
def start(module, fun, args, options \\ []) do
|
||||||
GenServer.start(Agent.Server, {module, fun, args}, options)
|
GenServer.start(Agent.Server, {module, fun, args}, options)
|
||||||
end
|
end
|
||||||
@@ -327,7 +323,7 @@ defmodule Agent do
|
|||||||
passing the agent state. The result of the function invocation is
|
passing the agent state. The result of the function invocation is
|
||||||
returned from this function.
|
returned from this function.
|
||||||
|
|
||||||
`timeout` is a non-negative integer which specifies how many
|
`timeout` is an integer greater than zero which specifies how many
|
||||||
milliseconds are allowed before the agent executes the function and returns
|
milliseconds are allowed before the agent executes the function and returns
|
||||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||||
is received within the specified time, the function call fails and the caller
|
is received within the specified time, the function call fails and the caller
|
||||||
@@ -352,7 +348,7 @@ defmodule Agent do
|
|||||||
instead of an anonymous function. The state is added as first
|
instead of an anonymous function. The state is added as first
|
||||||
argument to the given list of arguments.
|
argument to the given list of arguments.
|
||||||
"""
|
"""
|
||||||
@spec get(agent, module, atom, [term], timeout) :: term
|
@spec get(agent, module, atom, [term], timeout) :: any
|
||||||
def get(agent, module, fun, args, timeout \\ 5000) do
|
def get(agent, module, fun, args, timeout \\ 5000) do
|
||||||
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
|
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
|
||||||
end
|
end
|
||||||
@@ -366,7 +362,7 @@ defmodule Agent do
|
|||||||
elements, the first being the value to return (that is, the "get" value)
|
elements, the first being the value to return (that is, the "get" value)
|
||||||
and the second one being the new state of the agent.
|
and the second one being the new state of the agent.
|
||||||
|
|
||||||
`timeout` is a non-negative integer which specifies how many
|
`timeout` is an integer greater than zero which specifies how many
|
||||||
milliseconds are allowed before the agent executes the function and returns
|
milliseconds are allowed before the agent executes the function and returns
|
||||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||||
is received within the specified time, the function call fails and the caller
|
is received within the specified time, the function call fails and the caller
|
||||||
@@ -393,7 +389,7 @@ defmodule Agent do
|
|||||||
instead of an anonymous function. The state is added as first
|
instead of an anonymous function. The state is added as first
|
||||||
argument to the given list of arguments.
|
argument to the given list of arguments.
|
||||||
"""
|
"""
|
||||||
@spec get_and_update(agent, module, atom, [term], timeout) :: term
|
@spec get_and_update(agent, module, atom, [term], timeout) :: any
|
||||||
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
|
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
|
||||||
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
|
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
|
||||||
end
|
end
|
||||||
@@ -407,7 +403,7 @@ defmodule Agent do
|
|||||||
|
|
||||||
This function always returns `:ok`.
|
This function always returns `:ok`.
|
||||||
|
|
||||||
`timeout` is a non-negative integer which specifies how many
|
`timeout` is an integer greater than zero which specifies how many
|
||||||
milliseconds are allowed before the agent executes the function and returns
|
milliseconds are allowed before the agent executes the function and returns
|
||||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||||
is received within the specified time, the function call fails and the caller
|
is received within the specified time, the function call fails and the caller
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Agent.Server do
|
defmodule Agent.Server do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Application do
|
defmodule Application do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A module for working with applications and defining application callbacks.
|
A module for working with applications and defining application callbacks.
|
||||||
@@ -17,7 +13,7 @@ defmodule Application do
|
|||||||
|
|
||||||
Developers typically interact with the application environment and its
|
Developers typically interact with the application environment and its
|
||||||
callback module. Therefore those will be the topics we will cover first
|
callback module. Therefore those will be the topics we will cover first
|
||||||
before jumping into details about the application resource file and life cycle.
|
before jumping into details about the application resource file and life-cycle.
|
||||||
|
|
||||||
## The application environment
|
## The application environment
|
||||||
|
|
||||||
@@ -58,12 +54,12 @@ defmodule Application do
|
|||||||
You can also change the application environment dynamically by using functions
|
You can also change the application environment dynamically by using functions
|
||||||
such as `put_env/3` and `delete_env/2`.
|
such as `put_env/3` and `delete_env/2`.
|
||||||
|
|
||||||
> #### Application environment in libraries {: .info}
|
> #### Environment in libraries {: .tip}
|
||||||
>
|
>
|
||||||
> If you are writing a library to be used by other developers,
|
> The config files `config/config.exs` and `config/runtime.exs`
|
||||||
> it is generally recommended to avoid the application environment, as the
|
> are rarely used by libraries. Libraries typically define their environment
|
||||||
> application environment is effectively a global storage. For more information,
|
> in the `application/0` function of their `mix.exs`. Configuration files
|
||||||
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
|
> are rather used by applications to configure their libraries.
|
||||||
|
|
||||||
> #### Reading the environment of other applications {: .warning}
|
> #### Reading the environment of other applications {: .warning}
|
||||||
>
|
>
|
||||||
@@ -204,7 +200,7 @@ defmodule Application do
|
|||||||
In the sections above, we have configured an application in the
|
In the sections above, we have configured an application in the
|
||||||
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
|
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
|
||||||
this configuration to create an [*application resource
|
this configuration to create an [*application resource
|
||||||
file*](https://www.erlang.org/doc/man/app), which is a file called
|
file*](https://www.erlang.org/doc/man/application.html), which is a file called
|
||||||
`APP_NAME.app`. For example, the application resource file of the OTP
|
`APP_NAME.app`. For example, the application resource file of the OTP
|
||||||
application `ex_unit` is called `ex_unit.app`.
|
application `ex_unit` is called `ex_unit.app`.
|
||||||
|
|
||||||
@@ -247,7 +243,7 @@ defmodule Application do
|
|||||||
invoked if it hasn't been done yet. Then, it checks if the dependencies listed
|
invoked if it hasn't been done yet. Then, it checks if the dependencies listed
|
||||||
in the `applications` key of the resource file are already started. Having at
|
in the `applications` key of the resource file are already started. Having at
|
||||||
least one dependency not started is an error condition. Functions like
|
least one dependency not started is an error condition. Functions like
|
||||||
`ensure_all_started/1` take care of starting an application and all of its
|
`ensure_all_started/1` takes care of starting an application and all of its
|
||||||
dependencies for you.
|
dependencies for you.
|
||||||
|
|
||||||
If the application does not have a callback module configured, starting is
|
If the application does not have a callback module configured, starting is
|
||||||
@@ -262,8 +258,8 @@ defmodule Application do
|
|||||||
Application.stop(:ex_unit)
|
Application.stop(:ex_unit)
|
||||||
#=> :ok
|
#=> :ok
|
||||||
|
|
||||||
Stopping an application without a callback module defined, is in practice a
|
Stopping an application without a callback module is defined, but except for
|
||||||
no-op, except for some system tracing.
|
some system tracing, it is in practice a no-op.
|
||||||
|
|
||||||
Stopping an application with a callback module has three steps:
|
Stopping an application with a callback module has three steps:
|
||||||
|
|
||||||
@@ -281,7 +277,7 @@ defmodule Application do
|
|||||||
invoked only after termination of the whole supervision tree.
|
invoked only after termination of the whole supervision tree.
|
||||||
|
|
||||||
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
|
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
|
||||||
will shut down every application in the reverse order they were started.
|
will shut down every application in the opposite order they had been started.
|
||||||
|
|
||||||
By default, a SIGTERM from the operating system will automatically translate to
|
By default, a SIGTERM from the operating system will automatically translate to
|
||||||
`System.stop/0`. You can also have more explicit control over operating system
|
`System.stop/0`. You can also have more explicit control over operating system
|
||||||
@@ -471,9 +467,6 @@ defmodule Application do
|
|||||||
|
|
||||||
* #{Enum.map_join(@application_keys, "\n * ", &"`#{inspect(&1)}`")}
|
* #{Enum.map_join(@application_keys, "\n * ", &"`#{inspect(&1)}`")}
|
||||||
|
|
||||||
For a description of all fields, see [Erlang's application
|
|
||||||
specification](https://www.erlang.org/doc/man/app).
|
|
||||||
|
|
||||||
Note the environment is not returned as it can be accessed via
|
Note the environment is not returned as it can be accessed via
|
||||||
`fetch_env/2`. Returns `nil` if the application is not loaded.
|
`fetch_env/2`. Returns `nil` if the application is not loaded.
|
||||||
"""
|
"""
|
||||||
@@ -507,7 +500,7 @@ defmodule Application do
|
|||||||
of all loaded applications. Returns `nil` if
|
of all loaded applications. Returns `nil` if
|
||||||
the module is not listed in any application spec.
|
the module is not listed in any application spec.
|
||||||
"""
|
"""
|
||||||
@spec get_application(module) :: app | nil
|
@spec get_application(atom) :: atom | nil
|
||||||
def get_application(module) when is_atom(module) do
|
def get_application(module) when is_atom(module) do
|
||||||
case :application.get_application(module) do
|
case :application.get_application(module) do
|
||||||
{:ok, app} -> app
|
{:ok, app} -> app
|
||||||
@@ -677,16 +670,23 @@ defmodule Application do
|
|||||||
> You must use this function to read only your own application
|
> You must use this function to read only your own application
|
||||||
> environment. Do not read the environment of other applications.
|
> environment. Do not read the environment of other applications.
|
||||||
|
|
||||||
|
> #### Application environment in libraries {: .info}
|
||||||
|
>
|
||||||
|
> If you are writing a library to be used by other developers,
|
||||||
|
> it is generally recommended to avoid the application environment, as the
|
||||||
|
> application environment is effectively a global storage. For more information,
|
||||||
|
> read our [library guidelines](library-guidelines.md).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
||||||
Since Mix configurations are commonly used to configure applications (including
|
Since Mix configurations are commonly used to configure applications, we will use
|
||||||
your dependencies), we will use this as a point of illustration.
|
this as a point of illustration.
|
||||||
|
|
||||||
Consider a new application `:my_app`. `:my_app` contains a database engine which
|
Consider a new application `:my_app`. `:my_app` contains a database engine which
|
||||||
supports a pool of databases. The database engine needs to know the configuration for
|
supports a pool of databases. The database engine needs to know the configuration for
|
||||||
each of those databases, and that configuration is supplied by key-value pairs in
|
each of those databases, and that configuration is supplied by key-value pairs in
|
||||||
environment of `:my_app`. For example, your `config/runtime.exs` file might have:
|
environment of `:my_app`.
|
||||||
|
|
||||||
config :my_app, Databases.RepoOne,
|
config :my_app, Databases.RepoOne,
|
||||||
# A database configuration
|
# A database configuration
|
||||||
@@ -696,7 +696,7 @@ defmodule Application do
|
|||||||
config :my_app, Databases.RepoTwo,
|
config :my_app, Databases.RepoTwo,
|
||||||
# Another database configuration (for the same OTP app)
|
# Another database configuration (for the same OTP app)
|
||||||
ip: "localhost",
|
ip: "localhost",
|
||||||
port: 20_717
|
port: 20717
|
||||||
|
|
||||||
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
|
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
|
||||||
|
|
||||||
@@ -714,11 +714,6 @@ defmodule Application do
|
|||||||
config = Application.get_env(:my_app, Databases.RepoOne)
|
config = Application.get_env(:my_app, Databases.RepoOne)
|
||||||
config[:ip]
|
config[:ip]
|
||||||
|
|
||||||
The sample `config/runtime.exs` above could be used both for `:my_app` to
|
|
||||||
configure itself but also to allow any application that depends on `:my_app`
|
|
||||||
to configure how it works. However, one should keep in mind the caveats described
|
|
||||||
in the `Application` module documentation: the application environment is global
|
|
||||||
state which should be avoided if possible.
|
|
||||||
"""
|
"""
|
||||||
@spec get_env(app, key, value) :: value
|
@spec get_env(app, key, value) :: value
|
||||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||||
@@ -798,12 +793,6 @@ defmodule Application do
|
|||||||
@doc """
|
@doc """
|
||||||
Puts the `value` in `key` for the given `app`.
|
Puts the `value` in `key` for the given `app`.
|
||||||
|
|
||||||
> #### Compile environment {: .warning}
|
|
||||||
>
|
|
||||||
> Do not use this function to change environment variables read
|
|
||||||
> via `Application.compile_env/2`. The compile environment must
|
|
||||||
> be exclusively set before compilation, in your config files.
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
|
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
|
||||||
@@ -819,35 +808,22 @@ defmodule Application do
|
|||||||
stick after the application is loaded and also on application reload.
|
stick after the application is loaded and also on application reload.
|
||||||
"""
|
"""
|
||||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||||
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
|
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||||
maybe_warn_on_app_env_key(app, key)
|
maybe_warn_on_app_env_key(app, key)
|
||||||
:application.set_env(app, key, value, opts)
|
:application.set_env(app, key, value, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Puts the environment for multiple applications at the same time.
|
Puts the environment for multiple apps at the same time.
|
||||||
|
|
||||||
The given config should not:
|
The given config should not:
|
||||||
|
|
||||||
* have the same application listed more than once
|
* have the same application listed more than once
|
||||||
* have the same key inside the same application listed more than once
|
* have the same key inside the same application listed more than once
|
||||||
|
|
||||||
If those conditions are not met, this function will raise.
|
If those conditions are not met, it will raise.
|
||||||
|
|
||||||
This function receives the same options as `put_env/4`. Returns `:ok`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Application.put_all_env(
|
|
||||||
my_app: [
|
|
||||||
key: :value,
|
|
||||||
another_key: :another_value
|
|
||||||
],
|
|
||||||
another_app: [
|
|
||||||
key: :value
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.9.0"
|
@doc since: "1.9.0"
|
||||||
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
|
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
|
||||||
@@ -861,7 +837,7 @@ defmodule Application do
|
|||||||
It receives the same options as `put_env/4`. Returns `:ok`.
|
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||||
"""
|
"""
|
||||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||||
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
|
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||||
maybe_warn_on_app_env_key(app, key)
|
maybe_warn_on_app_env_key(app, key)
|
||||||
:application.unset_env(app, key, opts)
|
:application.unset_env(app, key, opts)
|
||||||
end
|
end
|
||||||
@@ -871,10 +847,7 @@ defmodule Application do
|
|||||||
|
|
||||||
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
|
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
|
||||||
defp maybe_warn_on_app_env_key(app, key) do
|
defp maybe_warn_on_app_env_key(app, key) do
|
||||||
message = fn ->
|
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
|
||||||
"passing non-atom as application env key is deprecated, got: #{inspect(key)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
|
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -908,16 +881,16 @@ defmodule Application do
|
|||||||
@doc """
|
@doc """
|
||||||
Ensures the given `app` or `apps` and their child applications are started.
|
Ensures the given `app` or `apps` and their child applications are started.
|
||||||
|
|
||||||
The second argument is either the `t:restart_type/0` (for consistency with
|
The second argument is either the `t:restart_type/1` (for consistency with
|
||||||
`start/2`) or a keyword list.
|
`start/2`) or a keyword list.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
* `:type` - if the application should be started `:temporary` (default),
|
* `:type` - if the application should be started in `:permanent`,
|
||||||
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
|
`:temporary`, or `:transient`. See `t:restart_type/1` for more information.
|
||||||
|
|
||||||
* `:mode` - (since v1.15.0) if the applications should be started serially
|
* `:mode` - (since v1.15.0) if the applications should be started serially
|
||||||
(`:serial`, default) or concurrently (`:concurrent`).
|
or concurrently. This option requires Erlang/OTP 26+.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
|
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
|
||||||
@@ -926,11 +899,11 @@ defmodule Application do
|
|||||||
{:ok, [app]} | {:error, term}
|
{:ok, [app]} | {:error, term}
|
||||||
def ensure_all_started(app_or_apps, type_or_opts \\ [])
|
def ensure_all_started(app_or_apps, type_or_opts \\ [])
|
||||||
|
|
||||||
def ensure_all_started(app_or_apps, type) when is_atom(type) do
|
def ensure_all_started(app, type) when is_atom(type) do
|
||||||
ensure_all_started(app_or_apps, type: type)
|
ensure_all_started(app, type: type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
|
def ensure_all_started(app, opts) when is_atom(app) do
|
||||||
ensure_all_started([app], opts)
|
ensure_all_started([app], opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -938,7 +911,18 @@ defmodule Application do
|
|||||||
|
|
||||||
def ensure_all_started(apps, opts) when is_list(apps) and is_list(opts) do
|
def ensure_all_started(apps, opts) when is_list(apps) and is_list(opts) do
|
||||||
opts = Keyword.validate!(opts, type: :temporary, mode: :serial)
|
opts = Keyword.validate!(opts, type: :temporary, mode: :serial)
|
||||||
:application.ensure_all_started(apps, opts[:type], opts[:mode])
|
|
||||||
|
if function_exported?(:application, :ensure_all_started, 3) do
|
||||||
|
:application.ensure_all_started(apps, opts[:type], opts[:mode])
|
||||||
|
else
|
||||||
|
# TODO: Remove this clause when we require Erlang/OTP 26+
|
||||||
|
Enum.reduce_while(apps, {:ok, []}, fn app, {:ok, acc} ->
|
||||||
|
case :application.ensure_all_started(app, opts[:type]) do
|
||||||
|
{:ok, apps} -> {:cont, {:ok, apps ++ acc}}
|
||||||
|
{:error, e} -> {:halt, {:error, e}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -996,7 +980,7 @@ defmodule Application do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets the directory for `app`.
|
Gets the directory for app.
|
||||||
|
|
||||||
This information is returned based on the code path. Here is an
|
This information is returned based on the code path. Here is an
|
||||||
example:
|
example:
|
||||||
@@ -1061,8 +1045,7 @@ defmodule Application do
|
|||||||
Returns a list with information about the applications which are currently running.
|
Returns a list with information about the applications which are currently running.
|
||||||
"""
|
"""
|
||||||
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
|
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
|
||||||
def started_applications(timeout \\ 5000)
|
def started_applications(timeout \\ 5000) do
|
||||||
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
|
|
||||||
:application.which_applications(timeout)
|
:application.which_applications(timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1077,7 +1060,7 @@ defmodule Application do
|
|||||||
@doc """
|
@doc """
|
||||||
Formats the error reason returned by `start/2`,
|
Formats the error reason returned by `start/2`,
|
||||||
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
||||||
and returns a string.
|
returns a string.
|
||||||
"""
|
"""
|
||||||
@spec format_error(any) :: String.t()
|
@spec format_error(any) :: String.t()
|
||||||
def format_error(reason) do
|
def format_error(reason) do
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Atom do
|
defmodule Atom do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Atoms are constants whose values are their own name.
|
Atoms are constants whose values are their own name.
|
||||||
@@ -71,7 +67,7 @@ defmodule Atom do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Atom.to_charlist(:"An atom")
|
iex> Atom.to_charlist(:"An atom")
|
||||||
~c"An atom"
|
'An atom'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_charlist(atom) :: charlist
|
@spec to_charlist(atom) :: charlist
|
||||||
|
|||||||
+155
-812
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Behaviour do
|
defmodule Behaviour do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Mechanism for handling behaviours.
|
Mechanism for handling behaviours.
|
||||||
@@ -13,8 +9,7 @@ defmodule Behaviour do
|
|||||||
attributes.
|
attributes.
|
||||||
|
|
||||||
Instead of `MyModule.__behaviour__(:callbacks)`,
|
Instead of `MyModule.__behaviour__(:callbacks)`,
|
||||||
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
|
`MyModule.behaviour_info(:callbacks)` can be used.
|
||||||
is documented in `Module`.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
|
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Bitwise do
|
defmodule Bitwise do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A set of functions that perform calculations on bits.
|
A set of functions that perform calculations on bits.
|
||||||
|
|
||||||
All bitwise functions work only on integers, otherwise an
|
All bitwise functions work only on integers; otherwise an
|
||||||
`ArithmeticError` is raised. The functions `band/2`,
|
`ArithmeticError` is raised. The functions `band/2`,
|
||||||
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
|
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
|
||||||
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
|
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
|
||||||
|
|||||||
+48
-184
@@ -1,10 +1,4 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Calendar do
|
defmodule Calendar do
|
||||||
@strftime_max_width 1024
|
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This module defines the responsibilities for working with
|
This module defines the responsibilities for working with
|
||||||
calendars, dates, times and datetimes in Elixir.
|
calendars, dates, times and datetimes in Elixir.
|
||||||
@@ -60,20 +54,9 @@ defmodule Calendar do
|
|||||||
@typedoc """
|
@typedoc """
|
||||||
Microseconds with stored precision.
|
Microseconds with stored precision.
|
||||||
|
|
||||||
`value` always represents the total value in microseconds.
|
The precision represents the number of digits that must be used when
|
||||||
|
|
||||||
The `precision` represents the number of digits that must be used when
|
|
||||||
representing the microseconds to external format. If the precision is `0`,
|
representing the microseconds to external format. If the precision is `0`,
|
||||||
it means microseconds must be skipped. If the precision is `6`, it means
|
it means microseconds must be skipped.
|
||||||
that `value` represents exactly the number of microseconds to be used.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
* `{0, 0}` means no microseconds.
|
|
||||||
* `{1, 6}` means 1µs.
|
|
||||||
* `{1000, 6}` means 1000µs (which is 1ms but measured at the microsecond precision).
|
|
||||||
* `{1000, 3}` means 1ms (which is measured at the millisecond precision).
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer}
|
@type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer}
|
||||||
|
|
||||||
@@ -87,14 +70,14 @@ defmodule Calendar do
|
|||||||
@type zone_abbr :: String.t()
|
@type zone_abbr :: String.t()
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
The time zone UTC offset in ISO seconds for standard time.
|
The time zone UTC offset in seconds for standard time.
|
||||||
|
|
||||||
See also `t:std_offset/0`.
|
See also `t:std_offset/0`.
|
||||||
"""
|
"""
|
||||||
@type utc_offset :: integer
|
@type utc_offset :: integer
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
The time zone standard offset in ISO seconds (typically not zero in summer times).
|
The time zone standard offset in seconds (typically not zero in summer times).
|
||||||
|
|
||||||
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
|
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
|
||||||
"""
|
"""
|
||||||
@@ -106,7 +89,6 @@ defmodule Calendar do
|
|||||||
@typedoc "Any map or struct that contains the time fields."
|
@typedoc "Any map or struct that contains the time fields."
|
||||||
@type time :: %{
|
@type time :: %{
|
||||||
optional(any) => any,
|
optional(any) => any,
|
||||||
calendar: calendar,
|
|
||||||
hour: hour,
|
hour: hour,
|
||||||
minute: minute,
|
minute: minute,
|
||||||
second: second,
|
second: second,
|
||||||
@@ -165,22 +147,6 @@ defmodule Calendar do
|
|||||||
"""
|
"""
|
||||||
@type time_zone_database :: module()
|
@type time_zone_database :: module()
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Options for formatting dates and times with `strftime/3`.
|
|
||||||
"""
|
|
||||||
@type strftime_opts :: [
|
|
||||||
preferred_datetime: String.t(),
|
|
||||||
preferred_date: String.t(),
|
|
||||||
preferred_time: String.t(),
|
|
||||||
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
|
|
||||||
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
|
||||||
abbreviated_month_names:
|
|
||||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
|
||||||
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
|
||||||
abbreviated_day_of_week_names:
|
|
||||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
|
|
||||||
]
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns how many days there are in the given month of the given year.
|
Returns how many days there are in the given month of the given year.
|
||||||
"""
|
"""
|
||||||
@@ -206,15 +172,6 @@ defmodule Calendar do
|
|||||||
`starting_on` represents the starting day of the week. All
|
`starting_on` represents the starting day of the week. All
|
||||||
calendars must support at least the `:default` value. They may
|
calendars must support at least the `:default` value. They may
|
||||||
also support other values representing their days of the week.
|
also support other values representing their days of the week.
|
||||||
|
|
||||||
The value of `day_of_week` is an ordinal number meaning that a
|
|
||||||
value of `1` is defined to mean "first day of the week". It is
|
|
||||||
specifically not defined to mean `1` is `Monday`.
|
|
||||||
|
|
||||||
It is a requirement that `first_day_of_week` is less than `last_day_of_week`
|
|
||||||
and that `day_of_week` must be within that range. Therefore it can be said
|
|
||||||
that `day_of_week in first_day_of_week..last_day_of_week//1` must be
|
|
||||||
`true` for all values of `day_of_week`.
|
|
||||||
"""
|
"""
|
||||||
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
|
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
|
||||||
{day_of_week(), first_day_of_week :: non_neg_integer(),
|
{day_of_week(), first_day_of_week :: non_neg_integer(),
|
||||||
@@ -296,7 +253,7 @@ defmodule Calendar do
|
|||||||
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Defines the rollover moment for the calendar.
|
Define the rollover moment for the calendar.
|
||||||
|
|
||||||
This is the moment, in your calendar, when the current day ends
|
This is the moment, in your calendar, when the current day ends
|
||||||
and the next day starts.
|
and the next day starts.
|
||||||
@@ -381,34 +338,6 @@ defmodule Calendar do
|
|||||||
@doc since: "1.15.0"
|
@doc since: "1.15.0"
|
||||||
@callback iso_days_to_end_of_day(iso_days) :: iso_days
|
@callback iso_days_to_end_of_day(iso_days) :: iso_days
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts date by the given duration according to its calendar.
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts naive datetime by the given duration according to its calendar.
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@callback shift_naive_datetime(
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
second,
|
|
||||||
microsecond,
|
|
||||||
Duration.t()
|
|
||||||
) :: {year, month, day, hour, minute, second, microsecond}
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts time by the given duration according to its calendar.
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
|
|
||||||
{hour, minute, second, microsecond}
|
|
||||||
|
|
||||||
# General Helpers
|
# General Helpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -495,30 +424,25 @@ defmodule Calendar do
|
|||||||
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
|
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
|
||||||
if the option is not received
|
if the option is not received
|
||||||
|
|
||||||
* `:am_pm_names` - a function that receives either `:am` or `:pm`
|
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
|
||||||
(and also the datetime if the function is arity/2) and returns
|
|
||||||
the name of the period of the day, if the option is not received it defaults
|
the name of the period of the day, if the option is not received it defaults
|
||||||
to a function that returns `"am"` and `"pm"`, respectively
|
to a function that returns `"am"` and `"pm"`, respectively
|
||||||
|
|
||||||
* `:month_names` - a function that receives a number (and also the
|
* `:month_names` - a function that receives a number and returns the name of
|
||||||
datetime if the function is arity/2) and returns the name of
|
|
||||||
the corresponding month, if the option is not received it defaults to a
|
the corresponding month, if the option is not received it defaults to a
|
||||||
function that returns the month names in English
|
function that returns the month names in English
|
||||||
|
|
||||||
* `:abbreviated_month_names` - a function that receives a number (and also
|
* `:abbreviated_month_names` - a function that receives a number and returns the
|
||||||
the datetime if the function is arity/2) and returns the
|
|
||||||
abbreviated name of the corresponding month, if the option is not received it
|
abbreviated name of the corresponding month, if the option is not received it
|
||||||
defaults to a function that returns the abbreviated month names in English
|
defaults to a function that returns the abbreviated month names in English
|
||||||
|
|
||||||
* `:day_of_week_names` - a function that receives a number and (and also the
|
* `:day_of_week_names` - a function that receives a number and returns the name of
|
||||||
datetime if the function is arity/2) returns the name of
|
|
||||||
the corresponding day of week, if the option is not received it defaults to a
|
the corresponding day of week, if the option is not received it defaults to a
|
||||||
function that returns the day of week names in English
|
function that returns the day of week names in English
|
||||||
|
|
||||||
* `:abbreviated_day_of_week_names` - a function that receives a number (and also
|
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
|
||||||
the datetime if the function is arity/2) and returns the abbreviated name of
|
the abbreviated name of the corresponding day of week, if the option is not received
|
||||||
the corresponding day of week, if the option is not received it defaults to a
|
it defaults to a function that returns the abbreviated day of week names in English
|
||||||
function that returns the abbreviated day of week names in English
|
|
||||||
|
|
||||||
## Formatting syntax
|
## Formatting syntax
|
||||||
|
|
||||||
@@ -532,7 +456,6 @@ defmodule Calendar do
|
|||||||
* `%`: indicates the start of a formatted section
|
* `%`: indicates the start of a formatted section
|
||||||
* `<padding>`: set the padding (see below)
|
* `<padding>`: set the padding (see below)
|
||||||
* `<width>`: a number indicating the minimum size of the formatted section
|
* `<width>`: a number indicating the minimum size of the formatted section
|
||||||
(maximum #{@strftime_max_width})
|
|
||||||
* `<format>`: the format itself (see below)
|
* `<format>`: the format itself (see below)
|
||||||
|
|
||||||
### Accepted padding options
|
### Accepted padding options
|
||||||
@@ -553,7 +476,7 @@ defmodule Calendar do
|
|||||||
B | Full month name | January
|
B | Full month name | January
|
||||||
c | Preferred date+time representation | 2018-10-17 12:34:56
|
c | Preferred date+time representation | 2018-10-17 12:34:56
|
||||||
d | Day of the month | 01, 31
|
d | Day of the month | 01, 31
|
||||||
f | Microseconds (uses its precision for width and padding) | 000000, 999999, 0123
|
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
|
||||||
H | Hour using a 24-hour clock | 00, 23
|
H | Hour using a 24-hour clock | 00, 23
|
||||||
I | Hour using a 12-hour clock | 01, 12
|
I | Hour using a 12-hour clock | 01, 12
|
||||||
j | Day of the year | 001, 366
|
j | Day of the year | 001, 366
|
||||||
@@ -563,11 +486,11 @@ defmodule Calendar do
|
|||||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||||
q | Quarter | 1, 2, 3, 4
|
q | Quarter | 1, 2, 3, 4
|
||||||
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
|
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
|
||||||
S | Second | 00, 59
|
S | Second | 00, 59, 60
|
||||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||||
x | Preferred date (without time) representation | 2018-10-17
|
x | Preferred date (without time) representation | 2018-10-17
|
||||||
X | Preferred time (without date) representation | 12:34:56
|
X | Preferred time (without date) representation | 12:34:56
|
||||||
y | Year as 2-digits | -01, 01, 86, 18
|
y | Year as 2-digits | 01, 01, 86, 18
|
||||||
Y | Year | -0001, 0001, 1986
|
Y | Year | -0001, 0001, 1986
|
||||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||||
@@ -575,12 +498,6 @@ defmodule Calendar do
|
|||||||
|
|
||||||
Any other character will be interpreted as an invalid format and raise an error.
|
Any other character will be interpreted as an invalid format and raise an error.
|
||||||
|
|
||||||
### `%f` Microseconds
|
|
||||||
|
|
||||||
`%f` does not support width and padding modifiers. It will be formatted by truncating
|
|
||||||
the microseconds to the precision of the `microseconds` field of the struct, with a
|
|
||||||
minimum precision of 1.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Without user options:
|
Without user options:
|
||||||
@@ -624,20 +541,9 @@ defmodule Calendar do
|
|||||||
...>)
|
...>)
|
||||||
"серпень"
|
"серпень"
|
||||||
|
|
||||||
Microsecond formatting:
|
|
||||||
|
|
||||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
|
|
||||||
"19-08-26 13:52:06.0"
|
|
||||||
|
|
||||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048Z], "%y-%m-%d %H:%M:%S.%f")
|
|
||||||
"19-08-26 13:52:06.048"
|
|
||||||
|
|
||||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048531Z], "%y-%m-%d %H:%M:%S.%f")
|
|
||||||
"19-08-26 13:52:06.048531"
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
|
@spec strftime(map(), String.t(), keyword()) :: String.t()
|
||||||
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
||||||
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
||||||
parse(
|
parse(
|
||||||
@@ -671,13 +577,9 @@ defmodule Calendar do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||||
width = (width || 0) * 10 + (digit - ?0)
|
new_width = (width || 0) * 10 + (digit - ?0)
|
||||||
|
|
||||||
if width > @strftime_max_width do
|
parse_modifiers(rest, new_width, pad, parser_data)
|
||||||
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
|
|
||||||
end
|
|
||||||
|
|
||||||
parse_modifiers(rest, width, pad, parser_data)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# set default padding if none was specified
|
# set default padding if none was specified
|
||||||
@@ -694,12 +596,12 @@ defmodule Calendar do
|
|||||||
format_modifiers(rest, width, pad, datetime, format_options, acc)
|
format_modifiers(rest, width, pad, datetime, format_options, acc)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp am_pm(hour, format_options, datetime) when hour > 11 do
|
defp am_pm(hour, format_options) when hour > 11 do
|
||||||
apply_format(:pm, format_options.am_pm_names, datetime)
|
format_options.am_pm_names.(:pm)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp am_pm(hour, format_options, datetime) when hour <= 11 do
|
defp am_pm(hour, format_options) when hour <= 11 do
|
||||||
apply_format(:am, format_options.am_pm_names, datetime)
|
format_options.am_pm_names.(:am)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_pad(format) when format in ~c"aAbBpPZ", do: ?\s
|
defp default_pad(format) when format in ~c"aAbBpPZ", do: ?\s
|
||||||
@@ -712,7 +614,7 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Literally just %
|
# Literally just %
|
||||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc])
|
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Abbreviated name of day
|
# Abbreviated name of day
|
||||||
@@ -720,7 +622,7 @@ defmodule Calendar do
|
|||||||
result =
|
result =
|
||||||
datetime
|
datetime
|
||||||
|> Date.day_of_week()
|
|> Date.day_of_week()
|
||||||
|> apply_format(format_options.abbreviated_day_of_week_names, datetime)
|
|> format_options.abbreviated_day_of_week_names.()
|
||||||
|> pad_leading(width, pad)
|
|> pad_leading(width, pad)
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
@@ -731,7 +633,7 @@ defmodule Calendar do
|
|||||||
result =
|
result =
|
||||||
datetime
|
datetime
|
||||||
|> Date.day_of_week()
|
|> Date.day_of_week()
|
||||||
|> apply_format(format_options.day_of_week_names, datetime)
|
|> format_options.day_of_week_names.()
|
||||||
|> pad_leading(width, pad)
|
|> pad_leading(width, pad)
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
@@ -741,7 +643,7 @@ defmodule Calendar do
|
|||||||
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result =
|
||||||
datetime.month
|
datetime.month
|
||||||
|> apply_format(format_options.abbreviated_month_names, datetime)
|
|> format_options.abbreviated_month_names.()
|
||||||
|> pad_leading(width, pad)
|
|> pad_leading(width, pad)
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
@@ -749,10 +651,7 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Full month name
|
# Full month name
|
||||||
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
|
||||||
datetime.month
|
|
||||||
|> apply_format(format_options.month_names, datetime)
|
|
||||||
|> pad_leading(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
@@ -781,7 +680,7 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Day of the month
|
# Day of the month
|
||||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -800,45 +699,37 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Hour using a 24-hour clock
|
# Hour using a 24-hour clock
|
||||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Hour using a 12-hour clock
|
# Hour using a 12-hour clock
|
||||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Day of the year
|
# Day of the year
|
||||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Month
|
# Month
|
||||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Minute
|
# Minute
|
||||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# "AM" or "PM" (noon is "PM", midnight as "AM")
|
# "AM" or "PM" (noon is "PM", midnight as "AM")
|
||||||
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
|
||||||
datetime.hour
|
|
||||||
|> am_pm(format_options, datetime)
|
|
||||||
|> String.upcase()
|
|
||||||
|> pad_leading(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
@@ -847,7 +738,7 @@ defmodule Calendar do
|
|||||||
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result =
|
||||||
datetime.hour
|
datetime.hour
|
||||||
|> am_pm(format_options, datetime)
|
|> am_pm(format_options)
|
||||||
|> String.downcase()
|
|> String.downcase()
|
||||||
|> pad_leading(width, pad)
|
|> pad_leading(width, pad)
|
||||||
|
|
||||||
@@ -856,23 +747,19 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Quarter
|
# Quarter
|
||||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Second
|
# Second
|
||||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Day of the week
|
# Day of the week
|
||||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -922,25 +809,20 @@ defmodule Calendar do
|
|||||||
|
|
||||||
# Year as 2-digits
|
# Year as 2-digits
|
||||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||||
if datetime.year < 0 do
|
|
||||||
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
|
||||||
else
|
|
||||||
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
|
||||||
end
|
|
||||||
|
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Year
|
# Year
|
||||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||||
result =
|
{sign, year} =
|
||||||
if datetime.year < 0 do
|
if datetime.year < 0 do
|
||||||
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
{?-, -datetime.year}
|
||||||
else
|
else
|
||||||
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
{[], datetime.year}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -987,7 +869,7 @@ defmodule Calendar do
|
|||||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||||
|
|
||||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||||
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}"
|
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||||
parse(rest, datetime, format_options, [result | acc])
|
parse(rest, datetime, format_options, [result | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1006,19 +888,13 @@ defmodule Calendar do
|
|||||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp pad_preferred(result, width, pad) do
|
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||||
result
|
pad_preferred([pad | result], width, pad)
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|> pad_leading(width, pad)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp pad_preferred(result, _width, _pad), do: result
|
||||||
|
|
||||||
defp pad_leading(string, count, padding) do
|
defp pad_leading(string, count, padding) do
|
||||||
to_pad = count - String.length(string)
|
|
||||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
|
||||||
end
|
|
||||||
|
|
||||||
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
|
|
||||||
defp pad_leading_ascii(string, count, padding) do
|
|
||||||
to_pad = count - byte_size(string)
|
to_pad = count - byte_size(string)
|
||||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||||
end
|
end
|
||||||
@@ -1028,18 +904,6 @@ defmodule Calendar do
|
|||||||
defp do_pad_leading(count, padding, acc),
|
defp do_pad_leading(count, padding, acc),
|
||||||
do: do_pad_leading(count - 1, padding, [padding | acc])
|
do: do_pad_leading(count - 1, padding, [padding | acc])
|
||||||
|
|
||||||
defp apply_format(term, formatter, _datetime) when is_function(formatter, 1) do
|
|
||||||
formatter.(term)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_format(term, formatter, datetime) when is_function(formatter, 2) do
|
|
||||||
formatter.(term, datetime)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_format(_term, formatter, _datetime) do
|
|
||||||
raise ArgumentError, "formatter functions must be of arity 1 or 2, got: #{inspect(formatter)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp options(user_options) do
|
defp options(user_options) do
|
||||||
default_options = %{
|
default_options = %{
|
||||||
preferred_date: "%Y-%m-%d",
|
preferred_date: "%Y-%m-%d",
|
||||||
|
|||||||
+50
-160
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Date do
|
defmodule Date do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A Date struct and functions.
|
A Date struct and functions.
|
||||||
@@ -35,17 +31,16 @@ defmodule Date do
|
|||||||
|
|
||||||
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
||||||
and based on the `Date` struct fields. For proper comparison between
|
and based on the `Date` struct fields. For proper comparison between
|
||||||
dates, use the `compare/2`, `after?/2` and `before?/2` functions.
|
dates, use the `compare/2` function. The existence of the `compare/2`
|
||||||
The existence of the `compare/2` function in this module also allows
|
function in this module also allows using `Enum.min/2` and `Enum.max/2`
|
||||||
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
|
functions to get the minimum and maximum date of an `Enum`. For example:
|
||||||
maximum date of an `Enum`. For example:
|
|
||||||
|
|
||||||
iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date)
|
iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date)
|
||||||
~D[2017-03-31]
|
~D[2017-03-31]
|
||||||
|
|
||||||
## Using epochs
|
## Using epochs
|
||||||
|
|
||||||
The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates
|
The `add/2` and `diff/2` functions can be used for computing dates
|
||||||
or retrieving the number of days between instants. For example, if there
|
or retrieving the number of days between instants. For example, if there
|
||||||
is an interest in computing the number of days from the Unix epoch
|
is an interest in computing the number of days from the Unix epoch
|
||||||
(1970-01-01):
|
(1970-01-01):
|
||||||
@@ -53,10 +48,7 @@ defmodule Date do
|
|||||||
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
|
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
|
||||||
14716
|
14716
|
||||||
|
|
||||||
iex> Date.add(~D[1970-01-01], 14_716)
|
iex> Date.add(~D[1970-01-01], 14716)
|
||||||
~D[2010-04-17]
|
|
||||||
|
|
||||||
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
|
|
||||||
~D[2010-04-17]
|
~D[2010-04-17]
|
||||||
|
|
||||||
Those functions are optimized to deal with common epochs, such
|
Those functions are optimized to deal with common epochs, such
|
||||||
@@ -79,9 +71,8 @@ defmodule Date do
|
|||||||
A range of dates represents a discrete number of dates where
|
A range of dates represents a discrete number of dates where
|
||||||
the first and last values are dates with matching calendars.
|
the first and last values are dates with matching calendars.
|
||||||
|
|
||||||
Ranges of dates can be increasing (`first <= last`) and are
|
Ranges of dates can be either increasing (`first <= last`) or
|
||||||
always inclusive. For a decreasing range, use `range/3` with
|
decreasing (`first > last`). They are also always inclusive.
|
||||||
a step of -1 as third argument.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -101,6 +92,8 @@ defmodule Date do
|
|||||||
true
|
true
|
||||||
iex> Enum.take(range, 3)
|
iex> Enum.take(range, 3)
|
||||||
[~D[2001-01-01], ~D[2001-01-02], ~D[2001-01-03]]
|
[~D[2001-01-01], ~D[2001-01-02], ~D[2001-01-03]]
|
||||||
|
iex> for d <- Date.range(~D[2023-03-01], ~D[2023-04-01]), Date.day_of_week(d) == 7, do: d
|
||||||
|
[~D[2023-03-05], ~D[2023-03-12], ~D[2023-03-19], ~D[2023-03-26]]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.5.0"
|
@doc since: "1.5.0"
|
||||||
@@ -108,18 +101,8 @@ defmodule Date do
|
|||||||
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
|
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
|
||||||
{first_days, _} = to_iso_days(first)
|
{first_days, _} = to_iso_days(first)
|
||||||
{last_days, _} = to_iso_days(last)
|
{last_days, _} = to_iso_days(last)
|
||||||
|
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.16
|
||||||
step =
|
step = if first_days <= last_days, do: 1, else: -1
|
||||||
if first_days <= last_days do
|
|
||||||
1
|
|
||||||
else
|
|
||||||
IO.warn(
|
|
||||||
"a negative range was inferred for Date.range/2, call Date.range/3 instead with -1 as third argument"
|
|
||||||
)
|
|
||||||
|
|
||||||
-1
|
|
||||||
end
|
|
||||||
|
|
||||||
range(first, first_days, last, last_days, calendar, step)
|
range(first, first_days, last, last_days, calendar, step)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -160,7 +143,7 @@ defmodule Date do
|
|||||||
) do
|
) do
|
||||||
raise ArgumentError,
|
raise ArgumentError,
|
||||||
"both dates must have matching calendar and the step must be a " <>
|
"both dates must have matching calendar and the step must be a " <>
|
||||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}"
|
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp range(first, first_days, last, last_days, calendar, step) do
|
defp range(first, first_days, last, last_days, calendar, step) do
|
||||||
@@ -193,8 +176,9 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def utc_today(calendar) do
|
def utc_today(calendar) do
|
||||||
%{year: year, month: month, day: day} = DateTime.utc_now(calendar)
|
calendar
|
||||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
|> DateTime.utc_now()
|
||||||
|
|> DateTime.to_date()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -321,7 +305,7 @@ defmodule Date do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the given date to a string according to its calendar.
|
Converts the given date to a string according to its calendar.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> Date.to_string(~D[2000-02-28])
|
iex> Date.to_string(~D[2000-02-28])
|
||||||
"2000-02-28"
|
"2000-02-28"
|
||||||
@@ -399,7 +383,7 @@ defmodule Date do
|
|||||||
or other calendars in which the days also start at midnight.
|
or other calendars in which the days also start at midnight.
|
||||||
Attempting to convert dates from other calendars will raise an `ArgumentError`.
|
Attempting to convert dates from other calendars will raise an `ArgumentError`.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> Date.to_iso8601(~D[2000-02-28])
|
iex> Date.to_iso8601(~D[2000-02-28])
|
||||||
"2000-02-28"
|
"2000-02-28"
|
||||||
@@ -422,7 +406,7 @@ defmodule Date do
|
|||||||
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
|
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
|
||||||
date
|
date
|
||||||
|> convert!(Calendar.ISO)
|
|> convert!(Calendar.ISO)
|
||||||
|> to_iso8601(format)
|
|> to_iso8601()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -556,18 +540,14 @@ defmodule Date do
|
|||||||
"""
|
"""
|
||||||
@doc since: "1.4.0"
|
@doc since: "1.4.0"
|
||||||
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
|
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
|
||||||
def compare(
|
def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
|
||||||
%{year: year1, month: month1, day: day1, calendar: calendar},
|
%{year: year1, month: month1, day: day1} = date1
|
||||||
%{year: year2, month: month2, day: day2, calendar: calendar}
|
%{year: year2, month: month2, day: day2} = date2
|
||||||
) do
|
|
||||||
cond do
|
case {{year1, month1, day1}, {year2, month2, day2}} do
|
||||||
year1 > year2 -> :gt
|
{first, second} when first > second -> :gt
|
||||||
year1 < year2 -> :lt
|
{first, second} when first < second -> :lt
|
||||||
month1 > month2 -> :gt
|
_ -> :eq
|
||||||
month1 < month2 -> :lt
|
|
||||||
day1 > day2 -> :gt
|
|
||||||
day1 < day2 -> :lt
|
|
||||||
true -> :eq
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -589,7 +569,7 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first date is strictly earlier than the second.
|
Returns true if the first date is strictly earlier than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -608,7 +588,7 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first date is strictly later than the second.
|
Returns true if the first date is strictly later than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -637,7 +617,7 @@ defmodule Date do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
|
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
|
||||||
@@ -671,7 +651,7 @@ defmodule Date do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
|
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
|
||||||
@@ -695,15 +675,8 @@ defmodule Date do
|
|||||||
@doc """
|
@doc """
|
||||||
Adds the number of days to the given `date`.
|
Adds the number of days to the given `date`.
|
||||||
|
|
||||||
> #### Prefer `shift/2` {: .info}
|
The days are counted as Gregorian days. The date is returned in the same
|
||||||
>
|
calendar as it was given in.
|
||||||
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
|
|
||||||
>
|
|
||||||
> `add/2` always considers a day to be measured according to the
|
|
||||||
> `Calendar.ISO`.
|
|
||||||
|
|
||||||
The days are counted as Gregorian days, independent of the underlying
|
|
||||||
calendar. The date is returned in the same calendar as it was given in.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -721,7 +694,12 @@ defmodule Date do
|
|||||||
@spec add(Calendar.date(), integer()) :: t
|
@spec add(Calendar.date(), integer()) :: t
|
||||||
def add(%{calendar: Calendar.ISO} = date, days) do
|
def add(%{calendar: Calendar.ISO} = date, days) do
|
||||||
%{year: year, month: month, day: day} = date
|
%{year: year, month: month, day: day} = date
|
||||||
{year, month, day} = Calendar.ISO.shift_days({year, month, day}, days)
|
|
||||||
|
{year, month, day} =
|
||||||
|
Calendar.ISO.date_to_iso_days(year, month, day)
|
||||||
|
|> Kernel.+(days)
|
||||||
|
|> Calendar.ISO.date_from_iso_days()
|
||||||
|
|
||||||
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -770,81 +748,6 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts given `date` by `duration` according to its calendar.
|
|
||||||
|
|
||||||
Allowed units are: `:year`, `:month`, `:week`, `:day`.
|
|
||||||
|
|
||||||
When using the default ISO calendar, durations are collapsed and
|
|
||||||
applied in the order of months and then days:
|
|
||||||
|
|
||||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
|
||||||
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
|
|
||||||
|
|
||||||
When shifting by month, days are rounded down to the nearest valid date.
|
|
||||||
|
|
||||||
Raises an `ArgumentError` when called with time scale units.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Date.shift(~D[2016-01-03], month: 2)
|
|
||||||
~D[2016-03-03]
|
|
||||||
iex> Date.shift(~D[2016-01-30], month: -1)
|
|
||||||
~D[2015-12-30]
|
|
||||||
iex> Date.shift(~D[2016-01-31], year: 4, day: 1)
|
|
||||||
~D[2020-02-01]
|
|
||||||
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
|
|
||||||
~D[2016-03-03]
|
|
||||||
|
|
||||||
# leap years
|
|
||||||
iex> Date.shift(~D[2024-02-29], year: 1)
|
|
||||||
~D[2025-02-28]
|
|
||||||
iex> Date.shift(~D[2024-02-29], year: 4)
|
|
||||||
~D[2028-02-29]
|
|
||||||
|
|
||||||
# rounding down
|
|
||||||
iex> Date.shift(~D[2015-01-31], month: 1)
|
|
||||||
~D[2015-02-28]
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
|
|
||||||
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
|
|
||||||
def shift(%{calendar: calendar} = date, duration) do
|
|
||||||
%{year: year, month: month, day: day} = date
|
|
||||||
{year, month, day} = calendar.shift_date(year, month, day, __duration__!(duration))
|
|
||||||
%Date{calendar: calendar, year: year, month: month, day: day}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def __duration__!(%Duration{} = duration) do
|
|
||||||
duration
|
|
||||||
end
|
|
||||||
|
|
||||||
# This part is inlined by the compiler on constant values
|
|
||||||
def __duration__!(unit_pairs) do
|
|
||||||
Enum.each(unit_pairs, &validate_duration_unit!/1)
|
|
||||||
struct!(Duration, unit_pairs)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, _value})
|
|
||||||
when unit in [:hour, :minute, :second, :microsecond] do
|
|
||||||
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
|
|
||||||
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, value}) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
|
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
|
||||||
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
|
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
|
||||||
@@ -865,7 +768,7 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Calculates the ordinal day of the week of a given `date`.
|
Calculates the day of the week of a given `date`.
|
||||||
|
|
||||||
Returns the day of the week as an integer. For the ISO 8601
|
Returns the day of the week as an integer. For the ISO 8601
|
||||||
calendar (the default), it is an integer from 1 to 7, where
|
calendar (the default), it is an integer from 1 to 7, where
|
||||||
@@ -874,19 +777,10 @@ defmodule Date do
|
|||||||
An optional `starting_on` value may be supplied, which
|
An optional `starting_on` value may be supplied, which
|
||||||
configures the weekday the week starts on. The default value
|
configures the weekday the week starts on. The default value
|
||||||
for it is `:default`, which translates to `:monday` for the
|
for it is `:default`, which translates to `:monday` for the
|
||||||
built-in ISO 8601 calendar. Any other weekday may be used for
|
built-in ISO calendar. Any other weekday may be given to.
|
||||||
`starting_on`, in such cases, that weekday will be considered the first
|
|
||||||
day of the week, and therefore it will be assigned the ordinal number 1.
|
|
||||||
|
|
||||||
The other calendars, the value returned is an ordinal day of week.
|
|
||||||
For example, `1` may mean "first day of the week" and `7` is
|
|
||||||
defined to mean "seventh day of the week". Custom calendars may
|
|
||||||
also accept their own variations of the `starting_on` parameter
|
|
||||||
with their own meaning.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
# 2016-10-31 is a Monday and by default Monday is the first day of the week
|
|
||||||
iex> Date.day_of_week(~D[2016-10-31])
|
iex> Date.day_of_week(~D[2016-10-31])
|
||||||
1
|
1
|
||||||
iex> Date.day_of_week(~D[2016-11-01])
|
iex> Date.day_of_week(~D[2016-11-01])
|
||||||
@@ -896,7 +790,6 @@ defmodule Date do
|
|||||||
iex> Date.day_of_week(~D[-0015-10-30])
|
iex> Date.day_of_week(~D[-0015-10-30])
|
||||||
3
|
3
|
||||||
|
|
||||||
# 2016-10-31 is a Monday but, as we start the week on Sunday, now it returns 2
|
|
||||||
iex> Date.day_of_week(~D[2016-10-31], :sunday)
|
iex> Date.day_of_week(~D[2016-10-31], :sunday)
|
||||||
2
|
2
|
||||||
iex> Date.day_of_week(~D[2016-11-01], :sunday)
|
iex> Date.day_of_week(~D[2016-11-01], :sunday)
|
||||||
@@ -1051,7 +944,7 @@ defmodule Date do
|
|||||||
@doc """
|
@doc """
|
||||||
Calculates the quarter of the year of a given `date`.
|
Calculates the quarter of the year of a given `date`.
|
||||||
|
|
||||||
Returns the quarter of the year as an integer. For the ISO 8601
|
Returns the day of the year as an integer. For the ISO 8601
|
||||||
calendar (the default), it is an integer from 1 to 4.
|
calendar (the default), it is an integer from 1 to 4.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -1096,7 +989,13 @@ defmodule Date do
|
|||||||
def year_of_era(date)
|
def year_of_era(date)
|
||||||
|
|
||||||
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
|
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||||
calendar.year_of_era(year, month, day)
|
# TODO: Remove me on 1.17
|
||||||
|
# The behaviour implementation already warns on missing callback.
|
||||||
|
if function_exported?(calendar, :year_of_era, 3) do
|
||||||
|
calendar.year_of_era(year, month, day)
|
||||||
|
else
|
||||||
|
calendar.year_of_era(year)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -1175,17 +1074,8 @@ defmodule Date do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defimpl Inspect do
|
defimpl Inspect do
|
||||||
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
|
|
||||||
when calendar != Calendar.ISO or year in -9999..9999 do
|
|
||||||
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
|
|
||||||
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
|
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
|
||||||
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, #{inspect(calendar)})"
|
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp suffix(Calendar.ISO), do: ""
|
defp suffix(Calendar.ISO), do: ""
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Date.Range do
|
defmodule Date.Range do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Returns an inclusive range between dates.
|
Returns an inclusive range between dates.
|
||||||
@@ -37,19 +33,21 @@ defmodule Date.Range do
|
|||||||
first_in_iso_days: first_days,
|
first_in_iso_days: first_days,
|
||||||
last_in_iso_days: last_days,
|
last_in_iso_days: last_days,
|
||||||
step: step
|
step: step
|
||||||
},
|
} = range,
|
||||||
%Date{calendar: calendar} = date
|
%Date{calendar: calendar} = date
|
||||||
) do
|
) do
|
||||||
{days, _} = Date.to_iso_days(date)
|
{days, _} = Date.to_iso_days(date)
|
||||||
|
|
||||||
in_range? =
|
cond do
|
||||||
if step > 0 do
|
empty?(range) ->
|
||||||
first_days <= days and days <= last_days and rem(days - first_days, step) == 0
|
{:ok, false}
|
||||||
else
|
|
||||||
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, in_range?}
|
first_days <= last_days ->
|
||||||
|
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def member?(%Date.Range{step: _}, _) do
|
def member?(%Date.Range{step: _}, _) do
|
||||||
@@ -57,20 +55,11 @@ defmodule Date.Range do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Remove me on v2.0
|
# TODO: Remove me on v2.0
|
||||||
member? =
|
def member?(
|
||||||
quote generated: true do
|
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||||
member?(
|
date_range,
|
||||||
%{
|
date
|
||||||
__struct__: Date.Range,
|
) do
|
||||||
first_in_iso_days: var!(first_days),
|
|
||||||
last_in_iso_days: var!(last_days)
|
|
||||||
} =
|
|
||||||
var!(date_range),
|
|
||||||
var!(date)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def unquote(member?) do
|
|
||||||
step = if first_days <= last_days, do: 1, else: -1
|
step = if first_days <= last_days, do: 1, else: -1
|
||||||
member?(Map.put(date_range, :step, step), date)
|
member?(Map.put(date_range, :step, step), date)
|
||||||
end
|
end
|
||||||
@@ -86,7 +75,7 @@ defmodule Date.Range do
|
|||||||
step: step
|
step: step
|
||||||
} = range
|
} = range
|
||||||
) do
|
) do
|
||||||
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)}
|
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Remove me on v2.0
|
# TODO: Remove me on v2.0
|
||||||
@@ -102,7 +91,7 @@ defmodule Date.Range do
|
|||||||
[date_from_iso_days(current, calendar)]
|
[date_from_iso_days(current, calendar)]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp slice(current, step, remaining, calendar) when remaining > 1 do
|
defp slice(current, step, remaining, calendar) do
|
||||||
[
|
[
|
||||||
date_from_iso_days(current, calendar)
|
date_from_iso_days(current, calendar)
|
||||||
| slice(current + step, step, remaining - 1, calendar)
|
| slice(current + step, step, remaining - 1, calendar)
|
||||||
@@ -178,12 +167,8 @@ defmodule Date.Range do
|
|||||||
when step < 0 and first_days < last_days,
|
when step < 0 and first_days < last_days,
|
||||||
do: 0
|
do: 0
|
||||||
|
|
||||||
defp size(%Date.Range{
|
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}),
|
||||||
first_in_iso_days: first_days,
|
do: abs(div(last_days - first_days, step)) + 1
|
||||||
last_in_iso_days: last_days,
|
|
||||||
step: step
|
|
||||||
}),
|
|
||||||
do: div(last_days - first_days, step) + 1
|
|
||||||
|
|
||||||
# TODO: Remove me on v2.0
|
# TODO: Remove me on v2.0
|
||||||
defp size(
|
defp size(
|
||||||
@@ -193,16 +178,43 @@ defmodule Date.Range do
|
|||||||
step = if first_days <= last_days, do: 1, else: -1
|
step = if first_days <= last_days, do: 1, else: -1
|
||||||
size(Map.put(date_range, :step, step))
|
size(Map.put(date_range, :step, step))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp empty?(%Date.Range{
|
||||||
|
first_in_iso_days: first_days,
|
||||||
|
last_in_iso_days: last_days,
|
||||||
|
step: step
|
||||||
|
})
|
||||||
|
when step > 0 and first_days > last_days,
|
||||||
|
do: true
|
||||||
|
|
||||||
|
defp empty?(%Date.Range{
|
||||||
|
first_in_iso_days: first_days,
|
||||||
|
last_in_iso_days: last_days,
|
||||||
|
step: step
|
||||||
|
})
|
||||||
|
when step < 0 and first_days < last_days,
|
||||||
|
do: true
|
||||||
|
|
||||||
|
defp empty?(%Date.Range{step: _}), do: false
|
||||||
|
|
||||||
|
# TODO: Remove me on v2.0
|
||||||
|
defp empty?(
|
||||||
|
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||||
|
date_range
|
||||||
|
) do
|
||||||
|
step = if first_days <= last_days, do: 1, else: -1
|
||||||
|
empty?(Map.put(date_range, :step, step))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Inspect do
|
defimpl Inspect do
|
||||||
import Kernel, except: [inspect: 2]
|
import Kernel, except: [inspect: 2]
|
||||||
|
|
||||||
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
|
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
|
||||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
|
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
|
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
|
||||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
|
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule DateTime do
|
defmodule DateTime do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A datetime implementation with a time zone.
|
A datetime implementation with a time zone.
|
||||||
@@ -17,8 +13,8 @@ defmodule DateTime do
|
|||||||
|
|
||||||
Remember, comparisons in Elixir using `==/2`, `>/2`, `</2` and friends
|
Remember, comparisons in Elixir using `==/2`, `>/2`, `</2` and friends
|
||||||
are structural and based on the DateTime struct fields. For proper
|
are structural and based on the DateTime struct fields. For proper
|
||||||
comparison between datetimes, use the `compare/2`, `after?/2` and `before?/2` functions.
|
comparison between datetimes, use the `compare/2` function. The
|
||||||
The existence of the `compare/2` function in this module also allows
|
existence of the `compare/2` function in this module also allows
|
||||||
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
|
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
|
||||||
maximum datetime of an `Enum`. For example:
|
maximum datetime of an `Enum`. For example:
|
||||||
|
|
||||||
@@ -32,12 +28,7 @@ defmodule DateTime do
|
|||||||
## Time zone database
|
## Time zone database
|
||||||
|
|
||||||
Many functions in this module require a time zone database.
|
Many functions in this module require a time zone database.
|
||||||
A time zone database is a record of the UTC offsets that its locales have
|
By default, it uses the default time zone database returned by
|
||||||
used at various times in the past, are using, and are expected to use in the
|
|
||||||
future.
|
|
||||||
Because those plans can change, it needs to be periodically updated.
|
|
||||||
|
|
||||||
By default, `DateTime` uses the default time zone database returned by
|
|
||||||
`Calendar.get_time_zone_database/0`, which defaults to
|
`Calendar.get_time_zone_database/0`, which defaults to
|
||||||
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
|
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
|
||||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||||
@@ -52,7 +43,7 @@ defmodule DateTime do
|
|||||||
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
|
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
|
||||||
recommended for embedded devices
|
recommended for embedded devices
|
||||||
|
|
||||||
To use one of them, first make sure it is added as a dependency in `mix.exs`.
|
To use them, first make sure it is added as a dependency in `mix.exs`.
|
||||||
It can then be configured either via configuration:
|
It can then be configured either via configuration:
|
||||||
|
|
||||||
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
|
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
|
||||||
@@ -69,7 +60,7 @@ defmodule DateTime do
|
|||||||
a date and time at a given time zone". To understand precisely
|
a date and time at a given time zone". To understand precisely
|
||||||
what we mean, let's see an example.
|
what we mean, let's see an example.
|
||||||
|
|
||||||
Imagine someone in Poland who wants to schedule a meeting with someone
|
Imagine someone in Poland wants to schedule a meeting with someone
|
||||||
in Brazil in the next year. The meeting will happen at 2:30 AM
|
in Brazil in the next year. The meeting will happen at 2:30 AM
|
||||||
in the Polish time zone. At what time will the meeting happen in
|
in the Polish time zone. At what time will the meeting happen in
|
||||||
Brazil?
|
Brazil?
|
||||||
@@ -81,7 +72,7 @@ defmodule DateTime do
|
|||||||
rules, ultimately affecting the result. For example, a country may
|
rules, ultimately affecting the result. For example, a country may
|
||||||
choose to enter or abandon "Daylight Saving Time", which is a
|
choose to enter or abandon "Daylight Saving Time", which is a
|
||||||
process where we adjust the clock one hour forward or one hour
|
process where we adjust the clock one hour forward or one hour
|
||||||
back once per year. Whenever the rules change, the exact instant
|
back once per year. Whenener the rules change, the exact instant
|
||||||
that 2:30 AM in Polish time will be in Brazil may change.
|
that 2:30 AM in Polish time will be in Brazil may change.
|
||||||
|
|
||||||
In other words, whenever working with future DateTimes, there is
|
In other words, whenever working with future DateTimes, there is
|
||||||
@@ -92,18 +83,17 @@ defmodule DateTime do
|
|||||||
not a problem, because time zone rules do not change for past
|
not a problem, because time zone rules do not change for past
|
||||||
events.
|
events.
|
||||||
|
|
||||||
To make matters worse, it may be that 2:30 AM in Polish time
|
To make matters worse, it may be that the 2:30 AM in Polish time
|
||||||
does not actually even exist or it is ambiguous. If a certain
|
does not actually even exist or it is ambiguous. If a certain
|
||||||
time zone observes "Daylight Saving Time", they will move their
|
time zone observes "Daylight Saving Time", they will move their
|
||||||
clock forward once a year. When this happens, there is a whole
|
clock forward once a year. When this happens, there is a whole
|
||||||
hour that does not exist. Then, when they move the clock back,
|
hour that does not exist. Then, when they move the clock back,
|
||||||
there is a certain hour that will happen twice. So if you want to
|
there is a certain hour that will happen twice. So if you want
|
||||||
schedule a meeting when this shift back happens, you would need to
|
to schedule a meeting when this shift back happens, you would
|
||||||
explicitly say which occurrence of 2:30 AM you mean: the one in
|
need to explicitly say which of the 2:30 AM you precisely mean.
|
||||||
"Summer Time", which occurs before the shift, or the one
|
Applications that are date and time sensitive, need to take
|
||||||
in "Standard Time", which occurs after it. Applications that are
|
these scenarios into account and correctly communicate them to
|
||||||
date and time sensitive need to take these scenarios into account
|
users.
|
||||||
and correctly communicate them to users.
|
|
||||||
|
|
||||||
The good news is: Elixir contains all of the building blocks
|
The good news is: Elixir contains all of the building blocks
|
||||||
necessary to tackle those problems. The default timezone database
|
necessary to tackle those problems. The default timezone database
|
||||||
@@ -113,24 +103,6 @@ defmodule DateTime do
|
|||||||
query the database and return the relevant information. For
|
query the database and return the relevant information. For
|
||||||
example, look at how `DateTime.new/4` returns different results
|
example, look at how `DateTime.new/4` returns different results
|
||||||
based on the scenarios described in this section.
|
based on the scenarios described in this section.
|
||||||
|
|
||||||
## Converting between timezones
|
|
||||||
|
|
||||||
Bearing in mind the cautions above, and assuming you've brought in a full
|
|
||||||
timezone database, here are some examples of common shifts between time
|
|
||||||
zones.
|
|
||||||
|
|
||||||
# Local time to UTC
|
|
||||||
new_york = DateTime.from_naive!(~N[2023-06-26T09:30:00], "America/New_York")
|
|
||||||
#=> #DateTime<2023-06-26 09:30:00-04:00 EDT America/New_York>
|
|
||||||
|
|
||||||
utc = DateTime.shift_zone!(new_york, "Etc/UTC")
|
|
||||||
#=> ~U[2023-06-26 13:30:00Z]
|
|
||||||
|
|
||||||
# UTC to local time
|
|
||||||
DateTime.shift_zone!(utc, "Europe/Paris")
|
|
||||||
#=> #DateTime<2023-06-26 15:30:00+02:00 CEST Europe/Paris>
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
||||||
@@ -179,9 +151,6 @@ defmodule DateTime do
|
|||||||
truncate the resulting datetime. This is available
|
truncate the resulting datetime. This is available
|
||||||
since v1.15.0.
|
since v1.15.0.
|
||||||
|
|
||||||
The default unit if none gets passed is `:native`,
|
|
||||||
which results in a default resolution of microseconds.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> datetime = DateTime.utc_now()
|
iex> datetime = DateTime.utc_now()
|
||||||
@@ -205,7 +174,7 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the current datetime in UTC, supporting
|
Returns the current datetime in UTC, supporting
|
||||||
a specific calendar and precision.
|
a specific calendar and precision.
|
||||||
|
|
||||||
If you want the current time in Unix seconds,
|
If you want the current time in Unix seconds,
|
||||||
@@ -383,12 +352,13 @@ defmodule DateTime do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the given Unix time to `DateTime`.
|
Converts the given Unix time to `DateTime`.
|
||||||
|
|
||||||
The integer can be given in different unit, according to `System.convert_time_unit/3`,
|
The integer can be given in different unit
|
||||||
and it will be converted to microseconds internally, which is the maximum precision
|
according to `System.convert_time_unit/3` and it will
|
||||||
supported by `DateTime`. In other words, any precision higher than microseconds will
|
be converted to microseconds internally. Up to
|
||||||
lead to truncation.
|
253402300799 seconds is supported.
|
||||||
|
|
||||||
Unix times are always in UTC. Therefore the DateTime will be returned in UTC.
|
Unix times are always in UTC and therefore the DateTime
|
||||||
|
will be returned in UTC.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -720,9 +690,6 @@ defmodule DateTime do
|
|||||||
Other time zone databases can be passed as argument or set globally.
|
Other time zone databases can be passed as argument or set globally.
|
||||||
See the "Time zone database" section in the module docs.
|
See the "Time zone database" section in the module docs.
|
||||||
|
|
||||||
Shifting to the `"Etc/UTC"` time zone always succeeds without
|
|
||||||
consulting the `time_zone_database`.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||||
@@ -756,28 +723,6 @@ defmodule DateTime do
|
|||||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
|
|
||||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
|
||||||
calendar.naive_datetime_from_iso_days(iso_days_utc)
|
|
||||||
|
|
||||||
datetime = %DateTime{
|
|
||||||
calendar: calendar,
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: {microsecond, precision},
|
|
||||||
std_offset: 0,
|
|
||||||
utc_offset: 0,
|
|
||||||
zone_abbr: "UTC",
|
|
||||||
time_zone: "Etc/UTC"
|
|
||||||
}
|
|
||||||
|
|
||||||
{:ok, datetime}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
|
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
|
||||||
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
|
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
|
||||||
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
|
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
|
||||||
@@ -909,10 +854,8 @@ defmodule DateTime do
|
|||||||
The `datetime` is expected to be using the ISO calendar
|
The `datetime` is expected to be using the ISO calendar
|
||||||
with a year greater than or equal to 0.
|
with a year greater than or equal to 0.
|
||||||
|
|
||||||
It will return the integer with the given unit, according
|
It will return the integer with the given unit,
|
||||||
to `System.convert_time_unit/3`. If the given unit is different
|
according to `System.convert_time_unit/3`.
|
||||||
than microseconds, the returned value will be either truncated
|
|
||||||
or padded accordingly.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -932,7 +875,7 @@ defmodule DateTime do
|
|||||||
-17412508655
|
-17412508655
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_unix(Calendar.datetime(), :native | System.time_unit()) :: integer
|
@spec to_unix(Calendar.datetime(), System.time_unit()) :: integer
|
||||||
def to_unix(datetime, unit \\ :second)
|
def to_unix(datetime, unit \\ :second)
|
||||||
|
|
||||||
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
|
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
|
||||||
@@ -1060,18 +1003,15 @@ defmodule DateTime do
|
|||||||
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
|
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
|
||||||
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
|
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
|
||||||
|
|
||||||
|
Only supports converting datetimes which are in the ISO calendar,
|
||||||
|
attempting to convert datetimes from other calendars will raise.
|
||||||
You can also optionally specify an offset for the formatted string.
|
You can also optionally specify an offset for the formatted string.
|
||||||
If none is given, the one in the given `datetime` is used.
|
|
||||||
|
|
||||||
Only supports converting datetimes which are in the ISO calendar.
|
|
||||||
If another calendar is given, it is automatically converted to ISO.
|
|
||||||
It raises if not possible.
|
|
||||||
|
|
||||||
WARNING: the ISO 8601 datetime format does not contain the time zone nor
|
WARNING: the ISO 8601 datetime format does not contain the time zone nor
|
||||||
its abbreviation, which means information is lost when converting to such
|
its abbreviation, which means information is lost when converting to such
|
||||||
format.
|
format.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||||
@@ -1121,21 +1061,8 @@ defmodule DateTime do
|
|||||||
@spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t()
|
@spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t()
|
||||||
def to_iso8601(datetime, format \\ :extended, offset \\ nil)
|
def to_iso8601(datetime, format \\ :extended, offset \\ nil)
|
||||||
|
|
||||||
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset)
|
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, nil)
|
||||||
when format in [:extended, :basic] do
|
when format in [:extended, :basic] do
|
||||||
datetime
|
|
||||||
|> to_iso8601_iodata(format, offset)
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_iso8601(%{calendar: _} = datetime, format, offset)
|
|
||||||
when format in [:extended, :basic] do
|
|
||||||
datetime
|
|
||||||
|> convert!(Calendar.ISO)
|
|
||||||
|> to_iso8601(format, offset)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_iodata(datetime, format, nil) do
|
|
||||||
%{
|
%{
|
||||||
year: year,
|
year: year,
|
||||||
month: month,
|
month: month,
|
||||||
@@ -1149,51 +1076,35 @@ defmodule DateTime do
|
|||||||
std_offset: std_offset
|
std_offset: std_offset
|
||||||
} = datetime
|
} = datetime
|
||||||
|
|
||||||
[
|
datetime_to_string(year, month, day, hour, minute, second, microsecond, format) <>
|
||||||
datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format),
|
Calendar.ISO.offset_to_string(utc_offset, std_offset, time_zone, format)
|
||||||
Calendar.ISO.offset_to_iodata(utc_offset, std_offset, time_zone, format)
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_iso8601_iodata(
|
def to_iso8601(
|
||||||
%{microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime,
|
%{calendar: Calendar.ISO, microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime,
|
||||||
format,
|
format,
|
||||||
0
|
0
|
||||||
) do
|
)
|
||||||
|
when format in [:extended, :basic] do
|
||||||
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, 0)
|
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, 0)
|
||||||
|
|
||||||
[
|
datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
|
||||||
datetime_to_iodata(
|
"Z"
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
second,
|
|
||||||
{microsecond, precision},
|
|
||||||
format
|
|
||||||
),
|
|
||||||
?Z
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp to_iso8601_iodata(datetime, format, offset) do
|
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset)
|
||||||
|
when format in [:extended, :basic] do
|
||||||
{_, precision} = datetime.microsecond
|
{_, precision} = datetime.microsecond
|
||||||
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, offset)
|
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, offset)
|
||||||
|
|
||||||
[
|
datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
|
||||||
datetime_to_iodata(
|
Calendar.ISO.offset_to_string(offset, 0, nil, format)
|
||||||
year,
|
end
|
||||||
month,
|
|
||||||
day,
|
def to_iso8601(%{calendar: _} = datetime, format, offset) when format in [:extended, :basic] do
|
||||||
hour,
|
datetime
|
||||||
minute,
|
|> convert!(Calendar.ISO)
|
||||||
second,
|
|> to_iso8601(format, offset)
|
||||||
{microsecond, precision},
|
|
||||||
format
|
|
||||||
),
|
|
||||||
Calendar.ISO.offset_to_iodata(offset, 0, nil, format)
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp shift_by_offset(%{calendar: calendar} = datetime, offset) do
|
defp shift_by_offset(%{calendar: calendar} = datetime, offset) do
|
||||||
@@ -1202,16 +1113,14 @@ defmodule DateTime do
|
|||||||
datetime
|
datetime
|
||||||
|> to_iso_days()
|
|> to_iso_days()
|
||||||
# Subtract total original offset in order to get UTC and add the new offset
|
# Subtract total original offset in order to get UTC and add the new offset
|
||||||
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second)
|
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|
||||||
|> calendar.naive_datetime_from_iso_days()
|
|> calendar.naive_datetime_from_iso_days()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format) do
|
defp datetime_to_string(year, month, day, hour, minute, second, microsecond, format) do
|
||||||
[
|
Calendar.ISO.date_to_string(year, month, day, format) <>
|
||||||
Calendar.ISO.date_to_iodata(year, month, day, format),
|
"T" <>
|
||||||
?T,
|
Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
|
||||||
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -1280,7 +1189,7 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts from ISO8601 specifying both a calendar and a mode.
|
Converts to ISO8601 specifying both a calendar and a mode.
|
||||||
|
|
||||||
See `from_iso8601/2` for more information.
|
See `from_iso8601/2` for more information.
|
||||||
|
|
||||||
@@ -1321,9 +1230,9 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts a number of Gregorian seconds to a `DateTime` struct.
|
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||||
|
|
||||||
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use
|
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||||
`DateTime.shift_zone/3`.
|
`DateTime.shift_zone/3`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -1366,7 +1275,7 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds.
|
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -1410,12 +1319,7 @@ defmodule DateTime do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the given `datetime` to a string according to its calendar.
|
Converts the given `datetime` to a string according to its calendar.
|
||||||
|
|
||||||
Unfortunately, there is no standard that specifies rendering of a
|
### Examples
|
||||||
datetime with its complete time zone information, so Elixir uses a
|
|
||||||
custom (but relatively common) representation which appends the time
|
|
||||||
zone abbreviation and full name to the datetime.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||||
@@ -1523,7 +1427,7 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first datetime is strictly earlier than the second.
|
Returns true if the first datetime is strictly earlier than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -1542,7 +1446,7 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first datetime is strictly later than the second.
|
Returns true if the first datetime is strictly later than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -1571,11 +1475,6 @@ defmodule DateTime do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z])
|
|
||||||
10
|
|
||||||
|
|
||||||
This function also considers timezone offsets:
|
|
||||||
|
|
||||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||||
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
|
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
|
||||||
@@ -1601,15 +1500,15 @@ defmodule DateTime do
|
|||||||
def diff(datetime1, datetime2, unit \\ :second)
|
def diff(datetime1, datetime2, unit \\ :second)
|
||||||
|
|
||||||
def diff(datetime1, datetime2, :day) do
|
def diff(datetime1, datetime2, :day) do
|
||||||
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000)
|
diff(datetime1, datetime2, :second) |> div(86400)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(datetime1, datetime2, :hour) do
|
def diff(datetime1, datetime2, :hour) do
|
||||||
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000)
|
diff(datetime1, datetime2, :second) |> div(3600)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(datetime1, datetime2, :minute) do
|
def diff(datetime1, datetime2, :minute) do
|
||||||
diff(datetime1, datetime2, :microsecond) |> div(60_000_000)
|
diff(datetime1, datetime2, :second) |> div(60)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(
|
def diff(
|
||||||
@@ -1617,67 +1516,43 @@ defmodule DateTime do
|
|||||||
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2,
|
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2,
|
||||||
unit
|
unit
|
||||||
) do
|
) do
|
||||||
if not is_integer(unit) and
|
|
||||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
naive_diff =
|
naive_diff =
|
||||||
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
|
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
|
||||||
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
|
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
|
||||||
|
|
||||||
offset_diff = utc_offset2 + std_offset2 - (utc_offset1 + std_offset1)
|
offset_diff = utc_offset2 + std_offset2 - (utc_offset1 + std_offset1)
|
||||||
|
naive_diff + System.convert_time_unit(offset_diff, :second, unit)
|
||||||
System.convert_time_unit(naive_diff, :microsecond, unit) +
|
|
||||||
System.convert_time_unit(offset_diff, :second, unit)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Adds a specified amount of time to a `DateTime`.
|
Adds a specified amount of time to a `DateTime`.
|
||||||
|
|
||||||
> #### Prefer `shift/2` {: .info}
|
|
||||||
>
|
|
||||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
|
||||||
>
|
|
||||||
> `add/3` provides a lower-level API which only supports fixed units
|
|
||||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
|
||||||
> of a month depends on the current month). `add/3` always considers
|
|
||||||
> the unit to be computed according to the `Calendar.ISO`.
|
|
||||||
|
|
||||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||||
`t:System.time_unit/0` for convenience but ultimately they are
|
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||||
all converted to microseconds. Negative values will move backwards
|
will move backwards in time.
|
||||||
in time and the default precision is `:second`.
|
|
||||||
|
|
||||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
This function always consider the unit to be computed according
|
||||||
always succeeds without consulting the `time_zone_database`.
|
to the `Calendar.ISO`.
|
||||||
|
|
||||||
This function relies on a contiguous representation of time,
|
This function uses relies on a contiguous representation of time,
|
||||||
ignoring timezone changes. For example, if you add one day when there
|
ignoring the wall time and timezone changes. For example, if you add
|
||||||
are summer time/daylight saving time changes, it will also change the
|
one day when there are summer time/daylight saving time changes,
|
||||||
time forward or backward by one hour, so the elapsed time is precisely
|
it will also change the time forward or backward by one hour,
|
||||||
24 hours. Similarly, adding just a few seconds to a datetime just before
|
so the elapsed time is precisely 24 hours. Similarly, adding just
|
||||||
"spring forward" can cause wall time to increase by more than an hour.
|
a few seconds to a datetime just before "spring forward" can cause
|
||||||
|
wall time to increase by more than an hour.
|
||||||
|
|
||||||
While this means this function is precise in terms of elapsed time,
|
While this means this function is precise in terms of elapsed time,
|
||||||
its result may be confusing in certain use cases. For example, if a
|
its result may be misleading in certain use cases. For example, if a
|
||||||
user requests a meeting to happen every day at 15:00 and you use this
|
user requests a meeting to happen every day at 15:00 and you use this
|
||||||
function to compute all future meetings by adding day after day, this
|
function to compute all future meetings by adding day after day, this
|
||||||
function may change the meeting time to 14:00 or 16:00 if there are
|
function may change the meeting time to 14:00 or 16:00 if there are
|
||||||
changes to the current timezone.
|
changes to the current timezone. Computing of recurring datetimes is
|
||||||
|
not currently supported in Elixir's standard library but it is available
|
||||||
|
by third-party libraries.
|
||||||
|
|
||||||
In case you don't want these changes to happen automatically or you
|
### Examples
|
||||||
want to surface time zone conflicts to the user, you can add to
|
|
||||||
the datetime as a naive datetime and then use `from_naive/2`:
|
|
||||||
|
|
||||||
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
|
|
||||||
|
|
||||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
|
||||||
to deal with them accordingly.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||||
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
|
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
|
||||||
@@ -1733,28 +1608,30 @@ defmodule DateTime do
|
|||||||
add(datetime, amount_to_add * 60, :second, time_zone_database)
|
add(datetime, amount_to_add * 60, :second, time_zone_database)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add(%{calendar: calendar} = datetime, amount_to_add, unit, time_zone_database)
|
def add(datetime, amount_to_add, unit, time_zone_database) when is_integer(amount_to_add) do
|
||||||
when is_integer(amount_to_add) do
|
|
||||||
%{
|
%{
|
||||||
microsecond: {_, precision},
|
|
||||||
time_zone: time_zone,
|
|
||||||
utc_offset: utc_offset,
|
utc_offset: utc_offset,
|
||||||
std_offset: std_offset
|
std_offset: std_offset,
|
||||||
|
calendar: calendar,
|
||||||
|
microsecond: {_, precision}
|
||||||
} = datetime
|
} = datetime
|
||||||
|
|
||||||
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
|
if not is_integer(unit) and
|
||||||
|
unit not in ~w(second millisecond microsecond nanosecond)a do
|
||||||
raise ArgumentError,
|
raise ArgumentError,
|
||||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ppd = System.convert_time_unit(86400, :second, unit)
|
||||||
|
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
|
||||||
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
datetime
|
datetime
|
||||||
|> to_iso_days()
|
|> to_iso_days()
|
||||||
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|
# Subtract total offset in order to get UTC and add the integer for the addition
|
||||||
|> apply_tz_offset(utc_offset + std_offset)
|
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add - total_offset, ppd)
|
||||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
|> shift_zone_for_iso_days_utc(calendar, precision, datetime.time_zone, time_zone_database)
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
{:ok, result_datetime} ->
|
{:ok, result_datetime} ->
|
||||||
@@ -1767,127 +1644,6 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts given `datetime` by `duration` according to its calendar.
|
|
||||||
|
|
||||||
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
|
|
||||||
|
|
||||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
|
||||||
always succeeds without consulting the `time_zone_database`.
|
|
||||||
|
|
||||||
This operation is equivalent to shifting the datetime wall clock
|
|
||||||
(in other words, the value as someone in that timezone would see
|
|
||||||
on their watch), then applying the time zone offset to convert it
|
|
||||||
to UTC, and finally computing the new timezone in case of shifts.
|
|
||||||
This ensures `shift/3` always returns a valid datetime.
|
|
||||||
|
|
||||||
Consequently, time zones that observe "Daylight Saving Time"
|
|
||||||
or other changes, across summer/winter time will add/remove hours
|
|
||||||
from the resulting datetime:
|
|
||||||
|
|
||||||
dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen")
|
|
||||||
DateTime.shift(dt, hour: 1)
|
|
||||||
#=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
|
|
||||||
|
|
||||||
dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles")
|
|
||||||
DateTime.shift(dt, hour: 2)
|
|
||||||
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
|
|
||||||
|
|
||||||
Although the first example shows a difference of 2 hours when
|
|
||||||
comparing the wall clocks of the given datetime with the returned one,
|
|
||||||
due to the "spring forward" time jump, the actual elapsed time is
|
|
||||||
still exactly of 1 hour.
|
|
||||||
|
|
||||||
In case you don't want these changes to happen automatically or you
|
|
||||||
want to surface time zone conflicts to the user, you can shift
|
|
||||||
the datetime as a naive datetime and then use `from_naive/2`:
|
|
||||||
|
|
||||||
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
|
|
||||||
|
|
||||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
|
||||||
to deal with them accordingly.
|
|
||||||
|
|
||||||
## ISO calendar considerations
|
|
||||||
|
|
||||||
When using the default ISO calendar, durations are collapsed and
|
|
||||||
applied in the order of months, then seconds and microseconds:
|
|
||||||
|
|
||||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
|
||||||
* weeks, days and smaller units are collapsed into seconds and microseconds
|
|
||||||
|
|
||||||
When shifting by month, days are rounded down to the nearest valid date.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2)
|
|
||||||
~U[2016-03-01 00:00:00Z]
|
|
||||||
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4)
|
|
||||||
~U[2017-01-29 00:00:00Z]
|
|
||||||
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25)
|
|
||||||
~U[2015-12-31 23:35:00Z]
|
|
||||||
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4})
|
|
||||||
~U[2016-01-01 00:05:00.0005Z]
|
|
||||||
|
|
||||||
# leap years
|
|
||||||
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1)
|
|
||||||
~U[2025-02-28 00:00:00Z]
|
|
||||||
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4)
|
|
||||||
~U[2028-02-29 00:00:00Z]
|
|
||||||
|
|
||||||
# rounding down
|
|
||||||
iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1)
|
|
||||||
~U[2015-02-28 00:00:00Z]
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
|
|
||||||
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
|
|
||||||
|
|
||||||
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
|
|
||||||
%{
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: microsecond,
|
|
||||||
std_offset: std_offset,
|
|
||||||
utc_offset: utc_offset,
|
|
||||||
time_zone: time_zone
|
|
||||||
} = datetime
|
|
||||||
|
|
||||||
{year, month, day, hour, minute, second, {_, precision} = microsecond} =
|
|
||||||
calendar.shift_naive_datetime(
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
second,
|
|
||||||
microsecond,
|
|
||||||
__duration__!(duration)
|
|
||||||
)
|
|
||||||
|
|
||||||
result =
|
|
||||||
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|
|
||||||
|> apply_tz_offset(utc_offset + std_offset)
|
|
||||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
|
||||||
|
|
||||||
case result do
|
|
||||||
{:ok, result_datetime} ->
|
|
||||||
result_datetime
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
raise ArgumentError,
|
|
||||||
"cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <>
|
|
||||||
"database #{inspect(time_zone_database)}), reason: #{inspect(error)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
defdelegate __duration__!(params), to: Duration, as: :new!
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the given datetime with the microsecond field truncated to the given
|
Returns the given datetime with the microsecond field truncated to the given
|
||||||
precision (`:microsecond`, `:millisecond` or `:second`).
|
precision (`:microsecond`, `:millisecond` or `:second`).
|
||||||
@@ -1936,7 +1692,7 @@ defmodule DateTime do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||||
@@ -1965,7 +1721,7 @@ defmodule DateTime do
|
|||||||
if Calendar.compatible_calendars?(dt_calendar, calendar) do
|
if Calendar.compatible_calendars?(dt_calendar, calendar) do
|
||||||
result_datetime =
|
result_datetime =
|
||||||
datetime
|
datetime
|
||||||
|> to_iso_days()
|
|> to_iso_days
|
||||||
|> from_iso_days(datetime, calendar, precision)
|
|> from_iso_days(datetime, calendar, precision)
|
||||||
|
|
||||||
{:ok, result_datetime}
|
{:ok, result_datetime}
|
||||||
@@ -1983,7 +1739,7 @@ defmodule DateTime do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||||
@@ -2053,12 +1809,11 @@ defmodule DateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp apply_tz_offset(iso_days, offset) do
|
defp apply_tz_offset(iso_days, offset) do
|
||||||
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second)
|
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp from_map(%{} = datetime_map) do
|
defp from_map(%{} = datetime_map) do
|
||||||
%DateTime{
|
%DateTime{
|
||||||
calendar: datetime_map.calendar,
|
|
||||||
year: datetime_map.year,
|
year: datetime_map.year,
|
||||||
month: datetime_map.month,
|
month: datetime_map.month,
|
||||||
day: datetime_map.day,
|
day: datetime_map.day,
|
||||||
@@ -2145,8 +1900,7 @@ defmodule DateTime do
|
|||||||
)
|
)
|
||||||
|
|
||||||
case datetime do
|
case datetime do
|
||||||
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
|
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
|
||||||
when calendar != Calendar.ISO or year in -9999..9999 ->
|
|
||||||
"~U[" <> formatted <> suffix(calendar) <> "]"
|
"~U[" <> formatted <> suffix(calendar) <> "]"
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
|||||||
@@ -1,600 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
defmodule Duration do
|
|
||||||
@moduledoc """
|
|
||||||
Struct and functions for handling durations.
|
|
||||||
|
|
||||||
A `Duration` struct represents a collection of time scale units,
|
|
||||||
allowing for manipulation and calculation of durations.
|
|
||||||
|
|
||||||
Date and time scale units are represented as integers, allowing for
|
|
||||||
both positive and negative values.
|
|
||||||
|
|
||||||
Microseconds are represented using a tuple `{microsecond, precision}`.
|
|
||||||
This ensures compatibility with other calendar types implementing time,
|
|
||||||
such as `Time`, `DateTime`, and `NaiveDateTime`.
|
|
||||||
|
|
||||||
## Shifting
|
|
||||||
|
|
||||||
The most common use of durations in Elixir's standard library is to
|
|
||||||
"shift" the calendar types.
|
|
||||||
|
|
||||||
iex> Date.shift(~D[2016-01-03], month: 2)
|
|
||||||
~D[2016-03-03]
|
|
||||||
|
|
||||||
In the example above, `Date.shift/2` automatically converts the units
|
|
||||||
into a `Duration` struct, although one can also be given directly:
|
|
||||||
|
|
||||||
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
|
|
||||||
~D[2016-03-03]
|
|
||||||
|
|
||||||
It is important to note that shifting is not an arithmetic operation.
|
|
||||||
For example, adding `date + 1 month + 1 month` does not yield the same
|
|
||||||
result as `date + 2 months`. Let's see an example:
|
|
||||||
|
|
||||||
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
|
|
||||||
~D[2016-03-29]
|
|
||||||
|
|
||||||
iex> ~D[2016-01-31] |> Date.shift(month: 2)
|
|
||||||
~D[2016-03-31]
|
|
||||||
|
|
||||||
As you can see above, the results differ, which explains why operations
|
|
||||||
with durations are called "shift" rather than "add". This happens because,
|
|
||||||
once we add one month to `2016-01-31`, we get `2016-02-29`. Then adding
|
|
||||||
one extra month gives us `2016-03-29` instead of `2016-03-31`.
|
|
||||||
|
|
||||||
In particular, when applying durations to `Calendar.ISO` types:
|
|
||||||
|
|
||||||
* larger units (such as years and months) are applied before
|
|
||||||
smaller ones (such as weeks, hours, days, and so on)
|
|
||||||
|
|
||||||
* units are collapsed into months (`:year` and `:month`),
|
|
||||||
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
|
|
||||||
and microseconds (`:microsecond`) before they are applied
|
|
||||||
|
|
||||||
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
|
|
||||||
Therefore, 4 weeks _are not_ equivalent to 1 month
|
|
||||||
|
|
||||||
* in case of non-existing dates, the results are rounded down to the
|
|
||||||
nearest valid date
|
|
||||||
|
|
||||||
As the `shift/2` functions are calendar aware, they are guaranteed to return
|
|
||||||
valid date/times, considering leap years as well as DST in applicable time zones.
|
|
||||||
|
|
||||||
## Intervals
|
|
||||||
|
|
||||||
Durations in Elixir can be combined with stream operations to build intervals.
|
|
||||||
For example, to retrieve the next three Wednesdays starting from 17th April, 2024:
|
|
||||||
|
|
||||||
iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
|
|
||||||
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]
|
|
||||||
|
|
||||||
However, once again, it is important to remember that shifting a duration is not
|
|
||||||
arithmetic, so you may want to use the functions in this module depending on what
|
|
||||||
you want to achieve. Compare the results of both examples below:
|
|
||||||
|
|
||||||
# Adding one month after the other
|
|
||||||
iex> date = ~D[2016-01-31]
|
|
||||||
iex> duration = Duration.new!(month: 1)
|
|
||||||
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
|
|
||||||
iex> Enum.take(stream, 3)
|
|
||||||
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]
|
|
||||||
|
|
||||||
# Multiplying durations by an index
|
|
||||||
iex> date = ~D[2016-01-31]
|
|
||||||
iex> duration = Duration.new!(month: 1)
|
|
||||||
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
|
|
||||||
iex> Enum.take(stream, 3)
|
|
||||||
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]
|
|
||||||
|
|
||||||
The second example consistently points to the last day of the month,
|
|
||||||
as it performs operations on the duration, rather than shifting date
|
|
||||||
after date.
|
|
||||||
|
|
||||||
## Comparing durations
|
|
||||||
|
|
||||||
In order to accurately compare durations, you need to either compare
|
|
||||||
only certain fields or use a reference time instant. This is because
|
|
||||||
some fields are relative to others. For example, you may say that
|
|
||||||
1 month is the same as 30 days, but if you add both of these durations
|
|
||||||
to `~D[2015-02-01]`, you would get different results, as that month
|
|
||||||
has only 28 days.
|
|
||||||
|
|
||||||
Therefore, if you wish to compare durations, one option is to use
|
|
||||||
`Date.shift/2` (or `DateTime.shift/2` or similar), and then compare
|
|
||||||
the dates:
|
|
||||||
|
|
||||||
iex> date = ~D[2015-02-01]
|
|
||||||
iex> Date.compare(Date.shift(date, month: 1), Date.shift(date, day: 30))
|
|
||||||
:lt
|
|
||||||
|
|
||||||
Or alternatively convert the durations to a fixed unit by using `to_timeout/1`,
|
|
||||||
which supports durations only up to weeks, raising if it has the month or year
|
|
||||||
fields set.
|
|
||||||
|
|
||||||
iex> to_timeout(hour: 24) == to_timeout(day: 1)
|
|
||||||
true
|
|
||||||
"""
|
|
||||||
|
|
||||||
@moduledoc since: "1.17.0"
|
|
||||||
|
|
||||||
@derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]}
|
|
||||||
defstruct year: 0,
|
|
||||||
month: 0,
|
|
||||||
week: 0,
|
|
||||||
day: 0,
|
|
||||||
hour: 0,
|
|
||||||
minute: 0,
|
|
||||||
second: 0,
|
|
||||||
microsecond: {0, 0}
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
The microsecond component of a duration.
|
|
||||||
|
|
||||||
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
|
|
||||||
durations may represent negative amounts of time. The precision is
|
|
||||||
an integer from 0 to 6 holding the number of significant digits,
|
|
||||||
as in the calendar types.
|
|
||||||
"""
|
|
||||||
@type microsecond :: {value :: integer, precision :: 0..6}
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
The duration struct type.
|
|
||||||
"""
|
|
||||||
@type t :: %Duration{
|
|
||||||
year: integer,
|
|
||||||
month: integer,
|
|
||||||
week: integer,
|
|
||||||
day: integer,
|
|
||||||
hour: integer,
|
|
||||||
minute: integer,
|
|
||||||
second: integer,
|
|
||||||
microsecond: microsecond()
|
|
||||||
}
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
The unit pair type specifies a pair of a valid duration unit key and value.
|
|
||||||
"""
|
|
||||||
@type unit_pair ::
|
|
||||||
{:year, integer}
|
|
||||||
| {:month, integer}
|
|
||||||
| {:week, integer}
|
|
||||||
| {:day, integer}
|
|
||||||
| {:hour, integer}
|
|
||||||
| {:minute, integer}
|
|
||||||
| {:second, integer}
|
|
||||||
| {:microsecond, microsecond()}
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
|
|
||||||
"""
|
|
||||||
@type duration :: t | [unit_pair]
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Options for `Duration.to_string/2`.
|
|
||||||
"""
|
|
||||||
@type to_string_opts :: [
|
|
||||||
units: [
|
|
||||||
year: String.t(),
|
|
||||||
month: String.t(),
|
|
||||||
week: String.t(),
|
|
||||||
day: String.t(),
|
|
||||||
hour: String.t(),
|
|
||||||
minute: String.t(),
|
|
||||||
second: String.t()
|
|
||||||
],
|
|
||||||
separator: String.t()
|
|
||||||
]
|
|
||||||
|
|
||||||
@microseconds_per_second 1_000_000
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Creates a new `Duration` struct from given `unit_pairs`.
|
|
||||||
|
|
||||||
Raises an `ArgumentError` when called with invalid unit pairs.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
|
|
||||||
%Duration{year: 1, week: 3, hour: 4, second: 1}
|
|
||||||
iex> Duration.new!(second: 1, microsecond: {1000, 6})
|
|
||||||
%Duration{second: 1, microsecond: {1000, 6}}
|
|
||||||
iex> Duration.new!(month: 2)
|
|
||||||
%Duration{month: 2}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec new!(duration()) :: t
|
|
||||||
def new!(%Duration{} = duration) do
|
|
||||||
duration
|
|
||||||
end
|
|
||||||
|
|
||||||
def new!(unit_pairs) do
|
|
||||||
Enum.each(unit_pairs, &validate_unit!/1)
|
|
||||||
struct!(Duration, unit_pairs)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_unit!({:microsecond, {ms, precision}})
|
|
||||||
when is_integer(ms) and precision in 0..6 do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_unit!({:microsecond, microsecond}) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_unit!({unit, _value})
|
|
||||||
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_unit!({_unit, value}) when is_integer(value) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_unit!({unit, value}) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Adds units of given durations `d1` and `d2`.
|
|
||||||
|
|
||||||
Respects the highest microsecond precision of the two.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
|
|
||||||
%Duration{week: 2, day: 3}
|
|
||||||
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
|
|
||||||
%Duration{microsecond: {1000, 6}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec add(t, t) :: t
|
|
||||||
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
|
||||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
|
||||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
|
||||||
|
|
||||||
%Duration{
|
|
||||||
year: y1 + y2,
|
|
||||||
month: mo1 + mo2,
|
|
||||||
week: w1 + w2,
|
|
||||||
day: day1 + day2,
|
|
||||||
hour: h1 + h2,
|
|
||||||
minute: mi1 + mi2,
|
|
||||||
second: s1 + s2,
|
|
||||||
microsecond: {ms1 + ms2, max(p1, p2)}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Subtracts units of given durations `d1` and `d2`.
|
|
||||||
|
|
||||||
Respects the highest microsecond precision of the two.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
|
|
||||||
%Duration{week: 2, day: -1}
|
|
||||||
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
|
|
||||||
%Duration{microsecond: {-200, 6}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec subtract(t, t) :: t
|
|
||||||
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
|
||||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
|
||||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
|
||||||
|
|
||||||
%Duration{
|
|
||||||
year: y1 - y2,
|
|
||||||
month: mo1 - mo2,
|
|
||||||
week: w1 - w2,
|
|
||||||
day: day1 - day2,
|
|
||||||
hour: h1 - h2,
|
|
||||||
minute: mi1 - mi2,
|
|
||||||
second: s1 - s2,
|
|
||||||
microsecond: {ms1 - ms2, max(p1, p2)}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Multiplies `duration` units by given `integer`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
|
|
||||||
%Duration{day: 3, minute: 45, second: -30}
|
|
||||||
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
|
|
||||||
%Duration{microsecond: {600, 4}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec multiply(t, integer) :: t
|
|
||||||
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
|
|
||||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
|
||||||
|
|
||||||
%Duration{
|
|
||||||
year: y * integer,
|
|
||||||
month: mo * integer,
|
|
||||||
week: w * integer,
|
|
||||||
day: d * integer,
|
|
||||||
hour: h * integer,
|
|
||||||
minute: mi * integer,
|
|
||||||
second: s * integer,
|
|
||||||
microsecond: {ms * integer, p}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Negates `duration` units.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
|
|
||||||
%Duration{day: -1, minute: -15, second: 10}
|
|
||||||
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
|
|
||||||
%Duration{microsecond: {-500000, 4}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec negate(t) :: t
|
|
||||||
def negate(%Duration{microsecond: {ms, p}} = duration) do
|
|
||||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
|
||||||
|
|
||||||
%Duration{
|
|
||||||
year: -y,
|
|
||||||
month: -mo,
|
|
||||||
week: -w,
|
|
||||||
day: -d,
|
|
||||||
hour: -h,
|
|
||||||
minute: -mi,
|
|
||||||
second: -s,
|
|
||||||
microsecond: {-ms, p}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
|
|
||||||
|
|
||||||
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
|
|
||||||
|
|
||||||
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
|
|
||||||
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
|
|
||||||
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
|
|
||||||
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
|
|
||||||
|
|
||||||
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
|
|
||||||
|
|
||||||
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
|
|
||||||
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
|
|
||||||
iex> Duration.from_iso8601("P3Y-2MT3H")
|
|
||||||
{:ok, %Duration{year: 3, month: -2, hour: 3}}
|
|
||||||
iex> Duration.from_iso8601("-PT10H-30M")
|
|
||||||
{:ok, %Duration{hour: -10, minute: 30}}
|
|
||||||
iex> Duration.from_iso8601("PT4.650S")
|
|
||||||
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
|
|
||||||
def from_iso8601(string) when is_binary(string) do
|
|
||||||
case Calendar.ISO.parse_duration(string) do
|
|
||||||
{:ok, duration} ->
|
|
||||||
{:ok, new!(duration)}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Same as `from_iso8601/1` but raises an `ArgumentError`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
|
|
||||||
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
|
|
||||||
iex> Duration.from_iso8601!("P10D")
|
|
||||||
%Duration{day: 10}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@spec from_iso8601!(String.t()) :: t
|
|
||||||
def from_iso8601!(string) when is_binary(string) do
|
|
||||||
case from_iso8601(string) do
|
|
||||||
{:ok, duration} ->
|
|
||||||
duration
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Converts the given `duration` to a human readable representation.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
* `:units` - the units to be used alongside each duration component.
|
|
||||||
The default units follow the ISO 80000-3 standard:
|
|
||||||
|
|
||||||
[
|
|
||||||
year: "a",
|
|
||||||
month: "mo",
|
|
||||||
week: "wk",
|
|
||||||
day: "d",
|
|
||||||
hour: "h",
|
|
||||||
minute: "min",
|
|
||||||
second: "s"
|
|
||||||
]
|
|
||||||
|
|
||||||
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.to_string(Duration.new!(second: 30))
|
|
||||||
"30s"
|
|
||||||
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
|
|
||||||
"40d 12h 42min 12s"
|
|
||||||
|
|
||||||
By default, this function uses ISO 80000-3 units, which uses "a" for years.
|
|
||||||
But you can customize all units via the units option:
|
|
||||||
|
|
||||||
iex> Duration.to_string(Duration.new!(year: 3))
|
|
||||||
"3a"
|
|
||||||
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
|
|
||||||
"3y"
|
|
||||||
|
|
||||||
You may also choose the separator:
|
|
||||||
|
|
||||||
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
|
|
||||||
"40d, 12h, 42min, 12s"
|
|
||||||
|
|
||||||
A duration without components is rendered as "0s":
|
|
||||||
|
|
||||||
iex> Duration.to_string(Duration.new!([]))
|
|
||||||
"0s"
|
|
||||||
|
|
||||||
Microseconds are rendered as part of seconds with the appropriate precision:
|
|
||||||
|
|
||||||
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
|
|
||||||
"1.002s"
|
|
||||||
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
|
|
||||||
"-0.2000s"
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.18.0"
|
|
||||||
@spec to_string(t, to_string_opts) :: String.t()
|
|
||||||
def to_string(%Duration{} = duration, opts \\ []) do
|
|
||||||
units = Keyword.get(opts, :units, [])
|
|
||||||
separator = Keyword.get(opts, :separator, " ")
|
|
||||||
|
|
||||||
case to_string_year(duration, [], units) do
|
|
||||||
[] ->
|
|
||||||
"0" <> Keyword.get(units, :second, "s")
|
|
||||||
|
|
||||||
[part] ->
|
|
||||||
IO.iodata_to_binary(part)
|
|
||||||
|
|
||||||
parts ->
|
|
||||||
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_part(0, _units, _key, _default, acc),
|
|
||||||
do: acc
|
|
||||||
|
|
||||||
defp to_string_part(x, units, key, default, acc),
|
|
||||||
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
|
|
||||||
|
|
||||||
defp to_string_year(%{year: year} = duration, acc, units) do
|
|
||||||
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_month(%{month: month} = duration, acc, units) do
|
|
||||||
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_week(%{week: week} = duration, acc, units) do
|
|
||||||
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_day(%{day: day} = duration, acc, units) do
|
|
||||||
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_hour(%{hour: hour} = duration, acc, units) do
|
|
||||||
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_minute(%{minute: minute} = duration, acc, units) do
|
|
||||||
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
|
|
||||||
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
|
|
||||||
|
|
||||||
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Duration.to_iso8601(Duration.new!(year: 3))
|
|
||||||
"P3Y"
|
|
||||||
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
|
|
||||||
"P40DT12H42M12S"
|
|
||||||
iex> Duration.to_iso8601(Duration.new!(second: 30))
|
|
||||||
"PT30S"
|
|
||||||
|
|
||||||
iex> Duration.to_iso8601(Duration.new!([]))
|
|
||||||
"PT0S"
|
|
||||||
|
|
||||||
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
|
|
||||||
"PT1.002S"
|
|
||||||
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
|
|
||||||
"PT-0.2000S"
|
|
||||||
"""
|
|
||||||
|
|
||||||
@spec to_iso8601(t) :: String.t()
|
|
||||||
def to_iso8601(%Duration{} = duration) do
|
|
||||||
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
|
|
||||||
{[], []} -> "PT0S"
|
|
||||||
{date, time} -> IO.iodata_to_binary([?P, date, time])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
|
|
||||||
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
|
|
||||||
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp second_component(%{second: 0, microsecond: {0, _}}) do
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp second_component(%{second: second, microsecond: {ms, p}}) do
|
|
||||||
[second_component(second, ms, p), ?S]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp second_component(second, _ms, 0) do
|
|
||||||
Integer.to_string(second)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp second_component(second, ms, p) do
|
|
||||||
total_ms = second * @microseconds_per_second + ms
|
|
||||||
second = total_ms |> div(@microseconds_per_second) |> abs()
|
|
||||||
ms = total_ms |> rem(@microseconds_per_second) |> abs()
|
|
||||||
sign = if total_ms < 0, do: ?-, else: []
|
|
||||||
|
|
||||||
[
|
|
||||||
sign,
|
|
||||||
Integer.to_string(second),
|
|
||||||
?.,
|
|
||||||
Calendar.ISO.microseconds_to_iodata(ms, p)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
@compile {:inline, pair: 2}
|
|
||||||
defp pair(0, _key), do: []
|
|
||||||
defp pair(num, key), do: [Integer.to_string(num), key]
|
|
||||||
end
|
|
||||||
+243
-733
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule NaiveDateTime do
|
defmodule NaiveDateTime do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A NaiveDateTime struct (without a time zone) and functions.
|
A NaiveDateTime struct (without a time zone) and functions.
|
||||||
@@ -40,10 +36,10 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
||||||
and based on the `NaiveDateTime` struct fields. For proper comparison
|
and based on the `NaiveDateTime` struct fields. For proper comparison
|
||||||
between naive datetimes, use the `compare/2`, `after?/2` and `before?/2` functions.
|
between naive datetimes, use the `compare/2` function. The existence of the
|
||||||
The existence of the `compare/2` function in this module also allows
|
`compare/2` function in this module also allows using `Enum.min/2` and
|
||||||
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
|
`Enum.max/2` functions to get the minimum and maximum naive datetime of an
|
||||||
maximum naive datetime of an `Enum`. For example:
|
`Enum`. For example:
|
||||||
|
|
||||||
iex> Enum.min([~N[2020-01-01 23:00:07], ~N[2000-01-01 23:00:07]], NaiveDateTime)
|
iex> Enum.min([~N[2020-01-01 23:00:07], ~N[2000-01-01 23:00:07]], NaiveDateTime)
|
||||||
~N[2000-01-01 23:00:07]
|
~N[2000-01-01 23:00:07]
|
||||||
@@ -114,12 +110,30 @@ defmodule NaiveDateTime do
|
|||||||
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
|
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
|
||||||
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
|
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
|
||||||
|
|
||||||
|
def utc_now(Calendar.ISO) do
|
||||||
|
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
|
||||||
|
Calendar.ISO.from_unix(:os.system_time(), :native)
|
||||||
|
|
||||||
|
%NaiveDateTime{
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
second: second,
|
||||||
|
microsecond: microsecond,
|
||||||
|
calendar: Calendar.ISO
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
|
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
|
||||||
utc_now(time_unit, Calendar.ISO)
|
utc_now(time_unit, Calendar.ISO)
|
||||||
end
|
end
|
||||||
|
|
||||||
def utc_now(calendar) do
|
def utc_now(calendar) do
|
||||||
utc_now(:native, calendar)
|
calendar
|
||||||
|
|> DateTime.utc_now()
|
||||||
|
|> DateTime.to_naive()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -144,27 +158,14 @@ defmodule NaiveDateTime do
|
|||||||
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
|
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
|
||||||
def utc_now(time_unit, calendar)
|
def utc_now(time_unit, calendar)
|
||||||
when time_unit in [:native, :microsecond, :millisecond, :second] do
|
when time_unit in [:native, :microsecond, :millisecond, :second] do
|
||||||
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
|
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
|
||||||
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
|
|
||||||
|
|
||||||
%NaiveDateTime{
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: microsecond,
|
|
||||||
calendar: Calendar.ISO
|
|
||||||
}
|
|
||||||
|> convert!(calendar)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the "local time" for the machine the Elixir program is running on.
|
Returns the "local time" for the machine the Elixir program is running on.
|
||||||
|
|
||||||
WARNING: This function can cause insidious bugs. It depends on the time zone
|
WARNING: This function can cause insidious bugs. It depends on the time zone
|
||||||
configuration at run time. This can change and be set to a time zone that has
|
configuration at run time. This can changed and be set to a time zone that has
|
||||||
daylight saving jumps (spring forward or fall back).
|
daylight saving jumps (spring forward or fall back).
|
||||||
|
|
||||||
This function can be used to display what the time is right now for the time
|
This function can be used to display what the time is right now for the time
|
||||||
@@ -251,7 +252,7 @@ defmodule NaiveDateTime do
|
|||||||
Calendar.hour(),
|
Calendar.hour(),
|
||||||
Calendar.minute(),
|
Calendar.minute(),
|
||||||
Calendar.second(),
|
Calendar.second(),
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
Calendar.microsecond() | non_neg_integer,
|
||||||
Calendar.calendar()
|
Calendar.calendar()
|
||||||
) :: {:ok, t} | {:error, atom}
|
) :: {:ok, t} | {:error, atom}
|
||||||
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||||
@@ -316,7 +317,7 @@ defmodule NaiveDateTime do
|
|||||||
Calendar.hour(),
|
Calendar.hour(),
|
||||||
Calendar.minute(),
|
Calendar.minute(),
|
||||||
Calendar.second(),
|
Calendar.second(),
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
Calendar.microsecond() | non_neg_integer,
|
||||||
Calendar.calendar()
|
Calendar.calendar()
|
||||||
) :: t
|
) :: t
|
||||||
def new!(
|
def new!(
|
||||||
@@ -391,20 +392,13 @@ defmodule NaiveDateTime do
|
|||||||
@doc """
|
@doc """
|
||||||
Adds a specified amount of time to a `NaiveDateTime`.
|
Adds a specified amount of time to a `NaiveDateTime`.
|
||||||
|
|
||||||
> #### Prefer `shift/2` {: .info}
|
|
||||||
>
|
|
||||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
|
||||||
>
|
|
||||||
> `add/3` provides a lower-level API which only supports fixed units
|
|
||||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
|
||||||
> of a month depends on the current month). `add/3` always considers
|
|
||||||
> the unit to be computed according to the `Calendar.ISO`.
|
|
||||||
|
|
||||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||||
`t:System.time_unit/0` for convenience but ultimately they are
|
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||||
all converted to microseconds. Negative values will move backwards
|
will move backwards in time.
|
||||||
in time and the default precision is `:second`.
|
|
||||||
|
This function always consider the unit to be computed according
|
||||||
|
to the `Calendar.ISO`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -472,21 +466,17 @@ defmodule NaiveDateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
%{calendar: calendar, microsecond: {_, precision}} = naive_datetime,
|
%{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
|
||||||
amount_to_add,
|
amount_to_add,
|
||||||
unit
|
unit
|
||||||
)
|
)
|
||||||
when is_integer(amount_to_add) do
|
when is_integer(amount_to_add) do
|
||||||
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
|
ppd = System.convert_time_unit(86400, :second, unit)
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
||||||
|
|
||||||
naive_datetime
|
naive_datetime
|
||||||
|> to_iso_days()
|
|> to_iso_days()
|
||||||
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add, ppd)
|
||||||
|> from_iso_days(calendar, precision)
|
|> from_iso_days(calendar, precision)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -541,15 +531,15 @@ defmodule NaiveDateTime do
|
|||||||
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
|
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
|
||||||
|
|
||||||
def diff(naive_datetime1, naive_datetime2, :day) do
|
def diff(naive_datetime1, naive_datetime2, :day) do
|
||||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000)
|
diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(naive_datetime1, naive_datetime2, :hour) do
|
def diff(naive_datetime1, naive_datetime2, :hour) do
|
||||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000)
|
diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(naive_datetime1, naive_datetime2, :minute) do
|
def diff(naive_datetime1, naive_datetime2, :minute) do
|
||||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000)
|
diff(naive_datetime1, naive_datetime2, :second) |> div(60)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(
|
def diff(
|
||||||
@@ -564,96 +554,11 @@ defmodule NaiveDateTime do
|
|||||||
"and thus the result would be ambiguous"
|
"and thus the result would be ambiguous"
|
||||||
end
|
end
|
||||||
|
|
||||||
if not is_integer(unit) and
|
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||||
raise ArgumentError,
|
units1 - units2
|
||||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
diff_microsecond =
|
|
||||||
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
|
|
||||||
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
|
|
||||||
|
|
||||||
System.convert_time_unit(diff_microsecond, :microsecond, unit)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Shifts given `naive_datetime` by `duration` according to its calendar.
|
|
||||||
|
|
||||||
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
|
|
||||||
|
|
||||||
When using the default ISO calendar, durations are collapsed and
|
|
||||||
applied in the order of months, then seconds and microseconds:
|
|
||||||
|
|
||||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
|
||||||
* weeks, days and smaller units are collapsed into seconds and microseconds
|
|
||||||
|
|
||||||
When shifting by month, days are rounded down to the nearest valid date.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1)
|
|
||||||
~N[2016-02-29 00:00:00]
|
|
||||||
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1)
|
|
||||||
~N[2020-02-01 00:00:00]
|
|
||||||
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1)
|
|
||||||
~N[2014-02-01 00:00:00]
|
|
||||||
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45)
|
|
||||||
~N[2016-01-31 00:00:45]
|
|
||||||
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6})
|
|
||||||
~N[2016-01-31 00:00:00.000100]
|
|
||||||
|
|
||||||
# leap years
|
|
||||||
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1)
|
|
||||||
~N[2025-02-28 00:00:00]
|
|
||||||
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4)
|
|
||||||
~N[2028-02-29 00:00:00]
|
|
||||||
|
|
||||||
# rounding down
|
|
||||||
iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1)
|
|
||||||
~N[2015-02-28 00:00:00]
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@spec shift(Calendar.naive_datetime(), Duration.duration()) :: t
|
|
||||||
def shift(%{calendar: calendar} = naive_datetime, duration) do
|
|
||||||
%{
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: microsecond
|
|
||||||
} = naive_datetime
|
|
||||||
|
|
||||||
{year, month, day, hour, minute, second, microsecond} =
|
|
||||||
calendar.shift_naive_datetime(
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
second,
|
|
||||||
microsecond,
|
|
||||||
__duration__!(duration)
|
|
||||||
)
|
|
||||||
|
|
||||||
%NaiveDateTime{
|
|
||||||
calendar: calendar,
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: microsecond
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
defdelegate __duration__!(params), to: Duration, as: :new!
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the given naive datetime with the microsecond field truncated to the
|
Returns the given naive datetime with the microsecond field truncated to the
|
||||||
given precision (`:microsecond`, `:millisecond` or `:second`).
|
given precision (`:microsecond`, `:millisecond` or `:second`).
|
||||||
@@ -674,7 +579,7 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.6.0"
|
@doc since: "1.6.0"
|
||||||
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t()
|
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
|
||||||
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
|
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
|
||||||
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
|
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
|
||||||
end
|
end
|
||||||
@@ -717,18 +622,16 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_date(Calendar.naive_datetime()) :: Date.t()
|
@spec to_date(Calendar.naive_datetime()) :: Date.t()
|
||||||
def to_date(
|
def to_date(%{
|
||||||
%{
|
year: year,
|
||||||
year: year,
|
month: month,
|
||||||
month: month,
|
day: day,
|
||||||
day: day,
|
calendar: calendar,
|
||||||
calendar: calendar,
|
hour: _,
|
||||||
hour: _,
|
minute: _,
|
||||||
minute: _,
|
second: _,
|
||||||
second: _,
|
microsecond: _
|
||||||
microsecond: _
|
}) do
|
||||||
} = _naive_datetime
|
|
||||||
) do
|
|
||||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -745,18 +648,16 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_time(Calendar.naive_datetime()) :: Time.t()
|
@spec to_time(Calendar.naive_datetime()) :: Time.t()
|
||||||
def to_time(
|
def to_time(%{
|
||||||
%{
|
year: _,
|
||||||
year: _,
|
month: _,
|
||||||
month: _,
|
day: _,
|
||||||
day: _,
|
calendar: calendar,
|
||||||
calendar: calendar,
|
hour: hour,
|
||||||
hour: hour,
|
minute: minute,
|
||||||
minute: minute,
|
second: second,
|
||||||
second: second,
|
microsecond: microsecond
|
||||||
microsecond: microsecond
|
}) do
|
||||||
} = _naive_datetime
|
|
||||||
) do
|
|
||||||
%Time{
|
%Time{
|
||||||
hour: hour,
|
hour: hour,
|
||||||
minute: minute,
|
minute: minute,
|
||||||
@@ -769,10 +670,7 @@ defmodule NaiveDateTime do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the given naive datetime to a string according to its calendar.
|
Converts the given naive datetime to a string according to its calendar.
|
||||||
|
|
||||||
For readability, this function follows the RFC3339 suggestion of removing
|
### Examples
|
||||||
the "T" separator between the date and time components.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
||||||
"2000-02-28 23:00:13"
|
"2000-02-28 23:00:13"
|
||||||
@@ -919,7 +817,7 @@ defmodule NaiveDateTime do
|
|||||||
Only supports converting naive datetimes which are in the ISO calendar,
|
Only supports converting naive datetimes which are in the ISO calendar,
|
||||||
attempting to convert naive datetimes from other calendars will raise.
|
attempting to convert naive datetimes from other calendars will raise.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
|
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
|
||||||
"2000-02-28T23:00:13"
|
"2000-02-28T23:00:13"
|
||||||
@@ -945,19 +843,6 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
def to_iso8601(%{calendar: Calendar.ISO} = naive_datetime, format)
|
def to_iso8601(%{calendar: Calendar.ISO} = naive_datetime, format)
|
||||||
when format in [:basic, :extended] do
|
when format in [:basic, :extended] do
|
||||||
naive_datetime
|
|
||||||
|> to_iso8601_iodata(format)
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_iso8601(%{calendar: _} = naive_datetime, format)
|
|
||||||
when format in [:basic, :extended] do
|
|
||||||
naive_datetime
|
|
||||||
|> convert!(Calendar.ISO)
|
|
||||||
|> to_iso8601(format)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp to_iso8601_iodata(naive_datetime, format) do
|
|
||||||
%{
|
%{
|
||||||
year: year,
|
year: year,
|
||||||
month: month,
|
month: month,
|
||||||
@@ -968,11 +853,14 @@ defmodule NaiveDateTime do
|
|||||||
microsecond: microsecond
|
microsecond: microsecond
|
||||||
} = naive_datetime
|
} = naive_datetime
|
||||||
|
|
||||||
[
|
Calendar.ISO.date_to_string(year, month, day, format) <>
|
||||||
Calendar.ISO.date_to_iodata(year, month, day, format),
|
"T" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
|
||||||
?T,
|
end
|
||||||
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
|
|
||||||
]
|
def to_iso8601(%{calendar: _} = naive_datetime, format) when format in [:basic, :extended] do
|
||||||
|
naive_datetime
|
||||||
|
|> convert!(Calendar.ISO)
|
||||||
|
|> to_iso8601(format)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -1016,8 +904,6 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
|
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
|
||||||
{:ok, ~N[2000-01-01 13:30:15]}
|
{:ok, ~N[2000-01-01 13:30:15]}
|
||||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
|
|
||||||
{:ok, ~N[2000-01-01 13:30:15.005000]}
|
|
||||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
||||||
{:ok, ~N[2000-01-01 13:30:15.005]}
|
{:ok, ~N[2000-01-01 13:30:15.005]}
|
||||||
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
|
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
|
||||||
@@ -1026,11 +912,7 @@ defmodule NaiveDateTime do
|
|||||||
{:error, :invalid_date}
|
{:error, :invalid_date}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec from_erl(
|
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
|
||||||
:calendar.datetime(),
|
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
|
||||||
Calendar.calendar()
|
|
||||||
) ::
|
|
||||||
{:ok, t} | {:error, atom}
|
{:ok, t} | {:error, atom}
|
||||||
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||||
|
|
||||||
@@ -1049,19 +931,13 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
|
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
|
||||||
~N[2000-01-01 13:30:15]
|
~N[2000-01-01 13:30:15]
|
||||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
|
|
||||||
~N[2000-01-01 13:30:15.005000]
|
|
||||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
||||||
~N[2000-01-01 13:30:15.005]
|
~N[2000-01-01 13:30:15.005]
|
||||||
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
|
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
|
||||||
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
|
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec from_erl!(
|
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||||
:calendar.datetime(),
|
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
|
||||||
Calendar.calendar()
|
|
||||||
) :: t
|
|
||||||
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||||
case from_erl(tuple, microsecond, calendar) do
|
case from_erl(tuple, microsecond, calendar) do
|
||||||
{:ok, value} ->
|
{:ok, value} ->
|
||||||
@@ -1088,28 +964,11 @@ defmodule NaiveDateTime do
|
|||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
|
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||||
def from_gregorian_seconds(seconds, microsecond_precision \\ {0, 0}, calendar \\ Calendar.ISO)
|
def from_gregorian_seconds(
|
||||||
|
seconds,
|
||||||
def from_gregorian_seconds(seconds, {microsecond, precision}, Calendar.ISO)
|
{microsecond, precision} \\ {0, 0},
|
||||||
when is_integer(seconds) do
|
calendar \\ Calendar.ISO
|
||||||
{days, seconds} = div_rem(seconds, 24 * 60 * 60)
|
)
|
||||||
{hours, seconds} = div_rem(seconds, 60 * 60)
|
|
||||||
{minutes, seconds} = div_rem(seconds, 60)
|
|
||||||
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
|
|
||||||
|
|
||||||
%NaiveDateTime{
|
|
||||||
calendar: Calendar.ISO,
|
|
||||||
year: year,
|
|
||||||
month: month,
|
|
||||||
day: day,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
second: seconds,
|
|
||||||
microsecond: {microsecond, precision}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_gregorian_seconds(seconds, {microsecond, precision}, calendar)
|
|
||||||
when is_integer(seconds) do
|
when is_integer(seconds) do
|
||||||
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
||||||
|
|
||||||
@@ -1128,17 +987,6 @@ defmodule NaiveDateTime do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp div_rem(int1, int2) do
|
|
||||||
div = div(int1, int2)
|
|
||||||
rem = int1 - div * int2
|
|
||||||
|
|
||||||
if rem >= 0 do
|
|
||||||
{div, rem}
|
|
||||||
else
|
|
||||||
{div - 1, rem + int2}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
|
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
|
||||||
|
|
||||||
@@ -1152,18 +1000,16 @@ defmodule NaiveDateTime do
|
|||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
||||||
def to_gregorian_seconds(
|
def to_gregorian_seconds(%{
|
||||||
%{
|
calendar: calendar,
|
||||||
calendar: calendar,
|
year: year,
|
||||||
year: year,
|
month: month,
|
||||||
month: month,
|
day: day,
|
||||||
day: day,
|
hour: hour,
|
||||||
hour: hour,
|
minute: minute,
|
||||||
minute: minute,
|
second: second,
|
||||||
second: second,
|
microsecond: {microsecond, precision}
|
||||||
microsecond: {microsecond, precision}
|
}) do
|
||||||
} = _naive_datetime
|
|
||||||
) do
|
|
||||||
{days, day_fraction} =
|
{days, day_fraction} =
|
||||||
calendar.naive_datetime_to_iso_days(
|
calendar.naive_datetime_to_iso_days(
|
||||||
year,
|
year,
|
||||||
@@ -1227,7 +1073,7 @@ defmodule NaiveDateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first `NaiveDateTime` is strictly earlier than the second.
|
Returns true if the first `NaiveDateTime` is strictly earlier than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -1246,7 +1092,7 @@ defmodule NaiveDateTime do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first `NaiveDateTime` is strictly later than the second.
|
Returns true if the first `NaiveDateTime` is strictly later than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -1274,7 +1120,7 @@ defmodule NaiveDateTime do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||||
@@ -1287,10 +1133,6 @@ defmodule NaiveDateTime do
|
|||||||
{:ok, t} | {:error, :incompatible_calendars}
|
{:ok, t} | {:error, :incompatible_calendars}
|
||||||
|
|
||||||
# Keep it multiline for proper function clause errors.
|
# Keep it multiline for proper function clause errors.
|
||||||
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
|
|
||||||
{:ok, ndt}
|
|
||||||
end
|
|
||||||
|
|
||||||
def convert(
|
def convert(
|
||||||
%{
|
%{
|
||||||
calendar: calendar,
|
calendar: calendar,
|
||||||
@@ -1322,7 +1164,7 @@ defmodule NaiveDateTime do
|
|||||||
if Calendar.compatible_calendars?(ndt_calendar, calendar) do
|
if Calendar.compatible_calendars?(ndt_calendar, calendar) do
|
||||||
result_naive_datetime =
|
result_naive_datetime =
|
||||||
naive_datetime
|
naive_datetime
|
||||||
|> to_iso_days()
|
|> to_iso_days
|
||||||
|> from_iso_days(calendar, precision)
|
|> from_iso_days(calendar, precision)
|
||||||
|
|
||||||
{:ok, result_naive_datetime}
|
{:ok, result_naive_datetime}
|
||||||
@@ -1340,7 +1182,7 @@ defmodule NaiveDateTime do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||||
@@ -1370,7 +1212,7 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
datetime
|
datetime
|
||||||
|> NaiveDateTime.beginning_of_day()
|
|> NaiveDateTime.beginning_of_day()
|
||||||
|> DateTime.from_naive(datetime.time_zone)
|
|> DateTime.from_naive(datetime.timezone)
|
||||||
|
|
||||||
Note that the beginning of the day may not exist or be ambiguous
|
Note that the beginning of the day may not exist or be ambiguous
|
||||||
in a given timezone, so you must handle those cases accordingly.
|
in a given timezone, so you must handle those cases accordingly.
|
||||||
@@ -1397,7 +1239,7 @@ defmodule NaiveDateTime do
|
|||||||
|
|
||||||
datetime
|
datetime
|
||||||
|> NaiveDateTime.end_of_day()
|
|> NaiveDateTime.end_of_day()
|
||||||
|> DateTime.from_naive(datetime.time_zone)
|
|> DateTime.from_naive(datetime.timezone)
|
||||||
|
|
||||||
Note that the end of the day may not exist or be ambiguous
|
Note that the end of the day may not exist or be ambiguous
|
||||||
in a given timezone, so you must handle those cases accordingly.
|
in a given timezone, so you must handle those cases accordingly.
|
||||||
@@ -1492,15 +1334,10 @@ defmodule NaiveDateTime do
|
|||||||
calendar: calendar
|
calendar: calendar
|
||||||
} = naive_datetime
|
} = naive_datetime
|
||||||
|
|
||||||
if calendar != Calendar.ISO or year in -9999..9999 do
|
formatted =
|
||||||
formatted =
|
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
||||||
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
|
||||||
|
|
||||||
"~N[" <> formatted <> suffix(calendar) <> "]"
|
"~N[" <> formatted <> suffix(calendar) <> "]"
|
||||||
else
|
|
||||||
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
|
|
||||||
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp suffix(Calendar.ISO), do: ""
|
defp suffix(Calendar.ISO), do: ""
|
||||||
|
|||||||
+62
-217
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Time do
|
defmodule Time do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A Time struct and functions.
|
A Time struct and functions.
|
||||||
@@ -35,10 +31,9 @@ defmodule Time do
|
|||||||
|
|
||||||
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
|
||||||
and based on the `Time` struct fields. For proper comparison between
|
and based on the `Time` struct fields. For proper comparison between
|
||||||
times, use the `compare/2`, `after?/2` and `before?/2` functions.
|
times, use the `compare/2` function. The existence of the `compare/2`
|
||||||
The existence of the `compare/2` function in this module also allows
|
function in this module also allows using `Enum.min/2` and `Enum.max/2`
|
||||||
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
|
functions to get the minimum and maximum time of an `Enum`. For example:
|
||||||
maximum time of an `Enum`. For example:
|
|
||||||
|
|
||||||
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
|
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
|
||||||
~T[10:00:07.001]
|
~T[10:00:07.001]
|
||||||
@@ -55,57 +50,23 @@ defmodule Time do
|
|||||||
calendar: Calendar.calendar()
|
calendar: Calendar.calendar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@parts_per_day 86_400_000_000
|
||||||
@seconds_per_day 24 * 60 * 60
|
@seconds_per_day 24 * 60 * 60
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the current time in UTC.
|
Returns the current time in UTC.
|
||||||
|
|
||||||
You can pass a time unit to automatically truncate the resulting time.
|
|
||||||
|
|
||||||
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> time = Time.utc_now()
|
iex> time = Time.utc_now()
|
||||||
iex> time.hour >= 0
|
iex> time.hour >= 0
|
||||||
true
|
true
|
||||||
|
|
||||||
iex> time = Time.utc_now(:second)
|
|
||||||
iex> time.microsecond
|
|
||||||
{0, 0}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.4.0"
|
@doc since: "1.4.0"
|
||||||
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
|
@spec utc_now(Calendar.calendar()) :: t
|
||||||
def utc_now(calendar_or_time_unit \\ Calendar.ISO) do
|
def utc_now(calendar \\ Calendar.ISO) do
|
||||||
case calendar_or_time_unit do
|
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(:os.system_time(), :native)
|
||||||
unit when unit in [:native, :microsecond, :millisecond, :second] ->
|
|
||||||
utc_now(unit, Calendar.ISO)
|
|
||||||
|
|
||||||
calendar ->
|
|
||||||
utc_now(:native, calendar)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns the current time in UTC, supporting a precision and a specific calendar.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> time = Time.utc_now(:microsecond, Calendar.ISO)
|
|
||||||
iex> time.hour >= 0
|
|
||||||
true
|
|
||||||
|
|
||||||
iex> time = Time.utc_now(:second, Calendar.ISO)
|
|
||||||
iex> time.microsecond
|
|
||||||
{0, 0}
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.19.0"
|
|
||||||
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
|
|
||||||
def utc_now(time_unit, calendar)
|
|
||||||
when time_unit in [:native, :microsecond, :millisecond, :second] do
|
|
||||||
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
|
|
||||||
{hour, minute, second} = time
|
{hour, minute, second} = time
|
||||||
|
|
||||||
iso_time = %Time{
|
iso_time = %Time{
|
||||||
@@ -146,9 +107,8 @@ defmodule Time do
|
|||||||
iex> Time.new(23, 59, 59, 1_000_000)
|
iex> Time.new(23, 59, 59, 1_000_000)
|
||||||
{:error, :invalid_time}
|
{:error, :invalid_time}
|
||||||
|
|
||||||
Invalid precision:
|
# Invalid precision
|
||||||
|
Time.new(23, 59, 59, {999_999, 10})
|
||||||
iex> Time.new(23, 59, 59, {999_999, 10})
|
|
||||||
{:error, :invalid_time}
|
{:error, :invalid_time}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -156,7 +116,7 @@ defmodule Time do
|
|||||||
Calendar.hour(),
|
Calendar.hour(),
|
||||||
Calendar.minute(),
|
Calendar.minute(),
|
||||||
Calendar.second(),
|
Calendar.second(),
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
Calendar.microsecond() | non_neg_integer,
|
||||||
Calendar.calendar()
|
Calendar.calendar()
|
||||||
) :: {:ok, t} | {:error, atom}
|
) :: {:ok, t} | {:error, atom}
|
||||||
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||||
@@ -226,7 +186,7 @@ defmodule Time do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the given `time` to a string.
|
Converts the given `time` to a string.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> Time.to_string(~T[23:00:00])
|
iex> Time.to_string(~T[23:00:00])
|
||||||
"23:00:00"
|
"23:00:00"
|
||||||
@@ -335,7 +295,7 @@ defmodule Time do
|
|||||||
format, for human readability. It also supports the "basic" format through
|
format, for human readability. It also supports the "basic" format through
|
||||||
passing the `:basic` option.
|
passing the `:basic` option.
|
||||||
|
|
||||||
## Examples
|
### Examples
|
||||||
|
|
||||||
iex> Time.to_iso8601(~T[23:00:13])
|
iex> Time.to_iso8601(~T[23:00:13])
|
||||||
"23:00:13"
|
"23:00:13"
|
||||||
@@ -396,21 +356,13 @@ defmodule Time do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Time.from_erl({23, 30, 15})
|
|
||||||
{:ok, ~T[23:30:15]}
|
|
||||||
iex> Time.from_erl({23, 30, 15}, 5000)
|
|
||||||
{:ok, ~T[23:30:15.005000]}
|
|
||||||
iex> Time.from_erl({23, 30, 15}, {5000, 3})
|
iex> Time.from_erl({23, 30, 15}, {5000, 3})
|
||||||
{:ok, ~T[23:30:15.005]}
|
{:ok, ~T[23:30:15.005]}
|
||||||
iex> Time.from_erl({24, 30, 15})
|
iex> Time.from_erl({24, 30, 15})
|
||||||
{:error, :invalid_time}
|
{:error, :invalid_time}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec from_erl(
|
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
|
||||||
:calendar.time(),
|
|
||||||
Calendar.microsecond() | non_neg_integer(),
|
|
||||||
Calendar.calendar()
|
|
||||||
) ::
|
|
||||||
{:ok, t} | {:error, atom}
|
{:ok, t} | {:error, atom}
|
||||||
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||||
|
|
||||||
@@ -426,8 +378,6 @@ defmodule Time do
|
|||||||
|
|
||||||
iex> Time.from_erl!({23, 30, 15})
|
iex> Time.from_erl!({23, 30, 15})
|
||||||
~T[23:30:15]
|
~T[23:30:15]
|
||||||
iex> Time.from_erl!({23, 30, 15}, 5000)
|
|
||||||
~T[23:30:15.005000]
|
|
||||||
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
|
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
|
||||||
~T[23:30:15.005]
|
~T[23:30:15.005]
|
||||||
iex> Time.from_erl!({24, 30, 15})
|
iex> Time.from_erl!({24, 30, 15})
|
||||||
@@ -467,12 +417,8 @@ defmodule Time do
|
|||||||
Calendar.microsecond(),
|
Calendar.microsecond(),
|
||||||
Calendar.calendar()
|
Calendar.calendar()
|
||||||
) :: t
|
) :: t
|
||||||
def from_seconds_after_midnight(
|
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||||
seconds,
|
when is_integer(seconds) do
|
||||||
{microsecond, precision} \\ {0, 0},
|
|
||||||
calendar \\ Calendar.ISO
|
|
||||||
)
|
|
||||||
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
|
|
||||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||||
|
|
||||||
{hour, minute, second, {_, _}} =
|
{hour, minute, second, {_, _}} =
|
||||||
@@ -483,7 +429,7 @@ defmodule Time do
|
|||||||
hour: hour,
|
hour: hour,
|
||||||
minute: minute,
|
minute: minute,
|
||||||
second: second,
|
second: second,
|
||||||
microsecond: {microsecond, precision}
|
microsecond: microsecond
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -501,7 +447,7 @@ defmodule Time do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()}
|
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||||
iso_days = {0, to_day_fraction(time)}
|
iso_days = {0, to_day_fraction(time)}
|
||||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||||
@@ -510,18 +456,13 @@ defmodule Time do
|
|||||||
@doc """
|
@doc """
|
||||||
Adds the `amount_to_add` of `unit`s to the given `time`.
|
Adds the `amount_to_add` of `unit`s to the given `time`.
|
||||||
|
|
||||||
> #### Prefer `shift/2` {: .info}
|
|
||||||
>
|
|
||||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
|
||||||
>
|
|
||||||
> `add/3` always considers the unit to be computed according to
|
|
||||||
> the `Calendar.ISO`.
|
|
||||||
|
|
||||||
Accepts an `amount_to_add` in any `unit`. `unit` can be
|
Accepts an `amount_to_add` in any `unit`. `unit` can be
|
||||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||||
`t:System.time_unit/0` for convenience but ultimately they are
|
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||||
all converted to microseconds. Negative values will move backwards
|
will move backwards in time.
|
||||||
in time and the default precision is `:second`.
|
|
||||||
|
This function always consider the unit to be computed according
|
||||||
|
to the `Calendar.ISO`.
|
||||||
|
|
||||||
Note the result value represents the time of day, meaning that it is cyclic,
|
Note the result value represents the time of day, meaning that it is cyclic,
|
||||||
for instance, it will never go over 24 hours for the ISO calendar.
|
for instance, it will never go over 24 hours for the ISO calendar.
|
||||||
@@ -574,26 +515,13 @@ defmodule Time do
|
|||||||
|
|
||||||
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
|
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
|
||||||
when is_integer(amount_to_add) do
|
when is_integer(amount_to_add) do
|
||||||
valid? =
|
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
|
||||||
if is_integer(unit),
|
total = time_to_microseconds(time) + amount_to_add
|
||||||
do: unit > 0,
|
parts = Integer.mod(total, @parts_per_day)
|
||||||
else: unit in ~w(second millisecond microsecond nanosecond)a
|
|
||||||
|
|
||||||
if not valid? do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
|
|
||||||
|
|
||||||
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
||||||
|
|
||||||
{hour, minute, second, {microsecond, _precision}} =
|
{hour, minute, second, {microsecond, _}} =
|
||||||
Calendar.ISO.shift_time_unit(
|
calendar.time_from_day_fraction({parts, @parts_per_day})
|
||||||
{hour, minute, second, microsecond},
|
|
||||||
amount_to_add,
|
|
||||||
unit
|
|
||||||
)
|
|
||||||
|
|
||||||
%Time{
|
%Time{
|
||||||
hour: hour,
|
hour: hour,
|
||||||
@@ -604,97 +532,25 @@ defmodule Time do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
defp time_to_microseconds(%{
|
||||||
Shifts given `time` by `duration` according to its calendar.
|
calendar: Calendar.ISO,
|
||||||
|
hour: 0,
|
||||||
Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`.
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
When using the default ISO calendar, durations are collapsed to seconds and
|
microsecond: {0, _}
|
||||||
microseconds before they are applied.
|
}) do
|
||||||
|
0
|
||||||
Raises an `ArgumentError` when called with date scale units.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Time.shift(~T[01:00:15], hour: 12)
|
|
||||||
~T[13:00:15]
|
|
||||||
iex> Time.shift(~T[01:35:00], hour: 6, minute: -15)
|
|
||||||
~T[07:20:00]
|
|
||||||
iex> Time.shift(~T[01:15:00], second: 125)
|
|
||||||
~T[01:17:05]
|
|
||||||
iex> Time.shift(~T[01:00:15], microsecond: {100, 6})
|
|
||||||
~T[01:00:15.000100]
|
|
||||||
iex> Time.shift(~T[01:15:00], Duration.new!(second: 65))
|
|
||||||
~T[01:16:05]
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.17.0"
|
|
||||||
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
|
|
||||||
when unit_pair:
|
|
||||||
{:hour, integer}
|
|
||||||
| {:minute, integer}
|
|
||||||
| {:second, integer}
|
|
||||||
| {:microsecond, {integer, 0..6}}
|
|
||||||
def shift(%{calendar: calendar} = time, duration) do
|
|
||||||
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
|
|
||||||
|
|
||||||
{hour, minute, second, microsecond} =
|
|
||||||
calendar.shift_time(hour, minute, second, microsecond, __duration__!(duration))
|
|
||||||
|
|
||||||
%Time{
|
|
||||||
calendar: calendar,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
second: second,
|
|
||||||
microsecond: microsecond
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
defp time_to_microseconds(time) do
|
||||||
def __duration__!(%Duration{} = duration) do
|
iso_days = {0, to_day_fraction(time)}
|
||||||
duration
|
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
|
||||||
end
|
|
||||||
|
|
||||||
# This part is inlined by the compiler on constant values
|
|
||||||
def __duration__!(unit_pairs) do
|
|
||||||
Enum.each(unit_pairs, &validate_duration_unit!/1)
|
|
||||||
struct!(Duration, unit_pairs)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({:microsecond, {ms, precision}})
|
|
||||||
when is_integer(ms) and precision in 0..6 do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({:microsecond, microsecond}) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, _value})
|
|
||||||
when unit not in [:hour, :minute, :second, :microsecond] do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_duration_unit!({unit, value}) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Compares two time structs.
|
Compares two time structs.
|
||||||
|
|
||||||
Returns `:gt` if the first time is later than the second
|
Returns `:gt` if first time is later than the second
|
||||||
and `:lt` for vice versa. If the two times are equal
|
and `:lt` for vice versa. If the two times are equal
|
||||||
`:eq` is returned.
|
`:eq` is returned.
|
||||||
|
|
||||||
@@ -720,32 +576,14 @@ defmodule Time do
|
|||||||
"""
|
"""
|
||||||
@doc since: "1.4.0"
|
@doc since: "1.4.0"
|
||||||
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
||||||
def compare(
|
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
|
||||||
%{
|
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
|
||||||
hour: hour1,
|
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
|
||||||
minute: minute1,
|
|
||||||
second: second1,
|
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
|
||||||
microsecond: {microsecond1, _},
|
{first, second} when first > second -> :gt
|
||||||
calendar: calendar
|
{first, second} when first < second -> :lt
|
||||||
},
|
_ -> :eq
|
||||||
%{
|
|
||||||
hour: hour2,
|
|
||||||
minute: minute2,
|
|
||||||
second: second2,
|
|
||||||
microsecond: {microsecond2, _},
|
|
||||||
calendar: calendar
|
|
||||||
}
|
|
||||||
) do
|
|
||||||
cond do
|
|
||||||
hour1 > hour2 -> :gt
|
|
||||||
hour1 < hour2 -> :lt
|
|
||||||
minute1 > minute2 -> :gt
|
|
||||||
minute1 < minute2 -> :lt
|
|
||||||
second1 > second2 -> :gt
|
|
||||||
second1 < second2 -> :lt
|
|
||||||
microsecond1 > microsecond2 -> :gt
|
|
||||||
microsecond1 < microsecond2 -> :lt
|
|
||||||
true -> :eq
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -761,7 +599,7 @@ defmodule Time do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first time is strictly earlier than the second.
|
Returns true if the first time is strictly earlier than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -780,7 +618,7 @@ defmodule Time do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns `true` if the first time is strictly later than the second.
|
Returns true if the first time is strictly later than the second.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -807,7 +645,7 @@ defmodule Time do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
|
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
|
||||||
@@ -863,7 +701,7 @@ defmodule Time do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||||
year:
|
year:
|
||||||
|
|
||||||
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
|
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
|
||||||
@@ -873,8 +711,15 @@ defmodule Time do
|
|||||||
@doc since: "1.5.0"
|
@doc since: "1.5.0"
|
||||||
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
|
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
|
||||||
def convert!(time, calendar) do
|
def convert!(time, calendar) do
|
||||||
{:ok, value} = convert(time, calendar)
|
case convert(time, calendar) do
|
||||||
value
|
{:ok, value} ->
|
||||||
|
value
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
raise ArgumentError,
|
||||||
|
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
|
||||||
|
"reason: #{inspect(reason)}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -924,11 +769,11 @@ defmodule Time do
|
|||||||
def diff(time1, time2, unit \\ :second)
|
def diff(time1, time2, unit \\ :second)
|
||||||
|
|
||||||
def diff(time1, time2, :hour) do
|
def diff(time1, time2, :hour) do
|
||||||
diff(time1, time2, :microsecond) |> div(3_600_000_000)
|
diff(time1, time2, :second) |> div(3600)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(time1, time2, :minute) do
|
def diff(time1, time2, :minute) do
|
||||||
diff(time1, time2, :microsecond) |> div(60_000_000)
|
diff(time1, time2, :second) |> div(60)
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff(
|
def diff(
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Calendar.TimeZoneDatabase do
|
defmodule Calendar.TimeZoneDatabase do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This module defines a behaviour for providing time zone data.
|
This module defines a behaviour for providing time zone data.
|
||||||
|
|||||||
+174
-492
File diff suppressed because it is too large
Load Diff
+138
-330
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Code.Formatter do
|
defmodule Code.Formatter do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
|
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
|
||||||
@@ -10,8 +6,7 @@ defmodule Code.Formatter do
|
|||||||
@double_heredoc "\"\"\""
|
@double_heredoc "\"\"\""
|
||||||
@single_quote "'"
|
@single_quote "'"
|
||||||
@single_heredoc "'''"
|
@single_heredoc "'''"
|
||||||
@sigil_c_double "~c\""
|
@sigil_c "~c\""
|
||||||
@sigil_c_single "~c'"
|
|
||||||
@sigil_c_heredoc "~c\"\"\""
|
@sigil_c_heredoc "~c\"\"\""
|
||||||
@newlines 2
|
@newlines 2
|
||||||
@min_line 0
|
@min_line 0
|
||||||
@@ -20,7 +15,7 @@ defmodule Code.Formatter do
|
|||||||
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
|
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
|
||||||
|
|
||||||
# Operators that are composed of multiple binary operators
|
# Operators that are composed of multiple binary operators
|
||||||
@multi_binary_operators [:..//]
|
@multi_binary_operators [:"..//"]
|
||||||
|
|
||||||
# Operators that do not have space between operands
|
# Operators that do not have space between operands
|
||||||
@no_space_binary_operators [:.., :"//"]
|
@no_space_binary_operators [:.., :"//"]
|
||||||
@@ -35,37 +30,30 @@ defmodule Code.Formatter do
|
|||||||
@right_new_line_before_binary_operators [:|, :when]
|
@right_new_line_before_binary_operators [:|, :when]
|
||||||
|
|
||||||
# Operators that are logical cannot be mixed without parens
|
# Operators that are logical cannot be mixed without parens
|
||||||
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
|
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
|
||||||
|
|
||||||
# Operators with next break fits
|
# Operators with next break fits
|
||||||
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
|
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
|
||||||
|
|
||||||
# Operators that always require parens even
|
# Operators that always require parens on operands when they are the parent
|
||||||
# when they are their own parents as they are not semantically associative
|
|
||||||
@required_parens_even_when_parent [:--, :---]
|
|
||||||
|
|
||||||
# Operators that always require parens on operands
|
|
||||||
# when they are the parent of another operator with a difference precedence
|
|
||||||
# Most operators are listed, except comparison, arithmetic, and low precedence
|
|
||||||
@required_parens_on_binary_operands [
|
@required_parens_on_binary_operands [
|
||||||
|
:|>,
|
||||||
:<<<,
|
:<<<,
|
||||||
:>>>,
|
:>>>,
|
||||||
:|>,
|
|
||||||
:<~,
|
:<~,
|
||||||
:~>,
|
:~>,
|
||||||
:<<~,
|
:<<~,
|
||||||
:~>>,
|
:~>>,
|
||||||
:<~>,
|
:<~>,
|
||||||
:"<|>",
|
:"<|>",
|
||||||
:in,
|
|
||||||
:"^^^",
|
:"^^^",
|
||||||
:"//",
|
|
||||||
:++,
|
|
||||||
:--,
|
|
||||||
:+++,
|
:+++,
|
||||||
:---,
|
:---,
|
||||||
:<>,
|
:in,
|
||||||
:..
|
:++,
|
||||||
|
:--,
|
||||||
|
:..,
|
||||||
|
:<>
|
||||||
]
|
]
|
||||||
|
|
||||||
@locals_without_parens [
|
@locals_without_parens [
|
||||||
@@ -158,7 +146,6 @@ defmodule Code.Formatter do
|
|||||||
@doc """
|
@doc """
|
||||||
Converts the quoted expression into an algebra document.
|
Converts the quoted expression into an algebra document.
|
||||||
"""
|
"""
|
||||||
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
|
|
||||||
def to_algebra(quoted, opts \\ []) do
|
def to_algebra(quoted, opts \\ []) do
|
||||||
comments = Keyword.get(opts, :comments, [])
|
comments = Keyword.get(opts, :comments, [])
|
||||||
|
|
||||||
@@ -194,12 +181,8 @@ defmodule Code.Formatter do
|
|||||||
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
|
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
|
||||||
file = Keyword.get(opts, :file, nil)
|
file = Keyword.get(opts, :file, nil)
|
||||||
sigils = Keyword.get(opts, :sigils, [])
|
sigils = Keyword.get(opts, :sigils, [])
|
||||||
migrate = Keyword.get(opts, :migrate, false)
|
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
|
||||||
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
|
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
|
||||||
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
|
|
||||||
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
|
|
||||||
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
|
|
||||||
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
|
|
||||||
syntax_colors = Keyword.get(opts, :syntax_colors, [])
|
syntax_colors = Keyword.get(opts, :syntax_colors, [])
|
||||||
|
|
||||||
sigils =
|
sigils =
|
||||||
@@ -224,11 +207,8 @@ defmodule Code.Formatter do
|
|||||||
comments: comments,
|
comments: comments,
|
||||||
sigils: sigils,
|
sigils: sigils,
|
||||||
file: file,
|
file: file,
|
||||||
migrate_atom_interpolations: migrate_atom_interpolations,
|
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
|
||||||
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
|
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
|
||||||
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
|
|
||||||
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
|
|
||||||
migrate_unless: migrate_unless,
|
|
||||||
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
|
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -285,7 +265,7 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
|
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
|
||||||
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
|
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
|
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
|
||||||
@@ -309,7 +289,6 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
|
|
||||||
defp quoted_to_algebra(
|
defp quoted_to_algebra(
|
||||||
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
|
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
|
||||||
context,
|
context,
|
||||||
@@ -320,7 +299,7 @@ defmodule Code.Formatter do
|
|||||||
remote_to_algebra(quoted, context, state)
|
remote_to_algebra(quoted, context, state)
|
||||||
|
|
||||||
meta[:delimiter] == ~s['''] ->
|
meta[:delimiter] == ~s['''] ->
|
||||||
{opener, quotes} = get_charlist_quotes(:heredoc, state)
|
{opener, quotes} = get_charlist_quotes(true, state)
|
||||||
|
|
||||||
{doc, state} =
|
{doc, state} =
|
||||||
entries
|
entries
|
||||||
@@ -330,26 +309,20 @@ defmodule Code.Formatter do
|
|||||||
{force_unfit(doc), state}
|
{force_unfit(doc), state}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{opener, quotes} = get_charlist_quotes({:regular, entries}, state)
|
{opener, quotes} = get_charlist_quotes(false, state)
|
||||||
list_interpolation_to_algebra(entries, quotes, state, opener, quotes)
|
list_interpolation_to_algebra(entries, quotes, state, opener, quotes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp quoted_to_algebra(
|
defp quoted_to_algebra(
|
||||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} =
|
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
|
||||||
quoted,
|
|
||||||
context,
|
context,
|
||||||
state
|
state
|
||||||
) do
|
) do
|
||||||
cond do
|
if interpolated?(entries) do
|
||||||
not interpolated?(entries) ->
|
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||||
remote_to_algebra(quoted, context, state)
|
else
|
||||||
|
remote_to_algebra(quoted, context, state)
|
||||||
state.migrate_atom_interpolations ->
|
|
||||||
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
|
|
||||||
|
|
||||||
true ->
|
|
||||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -392,22 +365,16 @@ defmodule Code.Formatter do
|
|||||||
tuple_to_algebra(meta, [left, right], :flex_break, state)
|
tuple_to_algebra(meta, [left, right], :flex_break, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
# (left -> right)
|
|
||||||
defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do
|
|
||||||
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
|
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
|
||||||
case meta[:delimiter] do
|
case meta[:delimiter] do
|
||||||
~s['''] ->
|
~s['''] ->
|
||||||
{opener, quotes} = get_charlist_quotes(:heredoc, state)
|
{opener, quotes} = get_charlist_quotes(true, state)
|
||||||
string = list |> List.to_string() |> escape_heredoc(quotes)
|
string = list |> List.to_string() |> escape_heredoc(quotes)
|
||||||
{opener |> concat(string) |> concat(quotes) |> force_unfit(), state}
|
{opener |> concat(string) |> concat(quotes) |> force_unfit(), state}
|
||||||
|
|
||||||
~s['] ->
|
~s['] ->
|
||||||
string = list |> List.to_string()
|
{opener, quotes} = get_charlist_quotes(false, state)
|
||||||
{opener, quotes} = get_charlist_quotes({:regular, [string]}, state)
|
string = list |> List.to_string() |> escape_string(quotes)
|
||||||
string = escape_string(string, quotes)
|
|
||||||
{opener |> concat(string) |> concat(quotes), state}
|
{opener |> concat(string) |> concat(quotes), state}
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
@@ -422,7 +389,7 @@ defmodule Code.Formatter do
|
|||||||
{@double_heredoc
|
{@double_heredoc
|
||||||
|> concat(string)
|
|> concat(string)
|
||||||
|> concat(@double_heredoc)
|
|> concat(@double_heredoc)
|
||||||
|> color_doc(:string, state.inspect_opts)
|
|> color(:string, state.inspect_opts)
|
||||||
|> force_unfit(), state}
|
|> force_unfit(), state}
|
||||||
else
|
else
|
||||||
string = escape_string(string, @double_quote)
|
string = escape_string(string, @double_quote)
|
||||||
@@ -430,7 +397,7 @@ defmodule Code.Formatter do
|
|||||||
{@double_quote
|
{@double_quote
|
||||||
|> concat(string)
|
|> concat(string)
|
||||||
|> concat(@double_quote)
|
|> concat(@double_quote)
|
||||||
|> color_doc(:string, state.inspect_opts), state}
|
|> color(:string, state.inspect_opts), state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -447,7 +414,6 @@ defmodule Code.Formatter do
|
|||||||
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
|
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# (unquote_splicing(...))
|
|
||||||
defp quoted_to_algebra(
|
defp quoted_to_algebra(
|
||||||
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
|
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
|
||||||
context,
|
context,
|
||||||
@@ -462,7 +428,7 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
|
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
|
||||||
{color_doc("nil", nil, state.inspect_opts), state}
|
{color("nil", nil, state.inspect_opts), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
|
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
|
||||||
@@ -479,7 +445,7 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|
||||||
|> color_doc(:atom, state.inspect_opts), state}
|
|> color(:atom, state.inspect_opts), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# &1
|
# &1
|
||||||
@@ -502,62 +468,6 @@ defmodule Code.Formatter do
|
|||||||
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
# disable migrate_call_parens_on_pipe within defmacro
|
|
||||||
defp quoted_to_algebra(
|
|
||||||
{atom, _, [{:|>, _, _}, _]} = ast,
|
|
||||||
context,
|
|
||||||
%{migrate_call_parens_on_pipe: true} = state
|
|
||||||
)
|
|
||||||
when atom in [:defmacro, :defmacrop] do
|
|
||||||
quoted_to_algebra(ast, context, %{state | migrate_call_parens_on_pipe: false})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp quoted_to_algebra(
|
|
||||||
{atom, _, [{:unless, _, _}, _]} = ast,
|
|
||||||
context,
|
|
||||||
%{migrate_unless: true} = state
|
|
||||||
)
|
|
||||||
when atom in [:defmacro, :defmacrop] do
|
|
||||||
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
|
|
||||||
end
|
|
||||||
|
|
||||||
# rewrite unless as if!
|
|
||||||
defp quoted_to_algebra(
|
|
||||||
{:unless, meta, [condition, block]},
|
|
||||||
context,
|
|
||||||
%{migrate_unless: true} = state
|
|
||||||
) do
|
|
||||||
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
|
|
||||||
end
|
|
||||||
|
|
||||||
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
|
|
||||||
defp quoted_to_algebra(
|
|
||||||
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
|
|
||||||
context,
|
|
||||||
%{migrate_unless: true} = state
|
|
||||||
) do
|
|
||||||
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
|
|
||||||
|
|
||||||
quoted_to_algebra(
|
|
||||||
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
|
|
||||||
context,
|
|
||||||
state
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# condition |> unless(...) => negated(condition) |> unless(...)
|
|
||||||
defp quoted_to_algebra(
|
|
||||||
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
|
|
||||||
context,
|
|
||||||
%{migrate_unless: true} = state
|
|
||||||
) do
|
|
||||||
quoted_to_algebra(
|
|
||||||
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
|
|
||||||
context,
|
|
||||||
state
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ..
|
# ..
|
||||||
defp quoted_to_algebra({:.., _meta, []}, context, state) do
|
defp quoted_to_algebra({:.., _meta, []}, context, state) do
|
||||||
if context in [:no_parens_arg, :no_parens_one_arg] do
|
if context in [:no_parens_arg, :no_parens_one_arg] do
|
||||||
@@ -567,13 +477,8 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ...
|
|
||||||
defp quoted_to_algebra({:..., _meta, []}, _context, state) do
|
|
||||||
{"...", state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# 1..2//3
|
# 1..2//3
|
||||||
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
|
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
|
||||||
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
|
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -592,6 +497,11 @@ defmodule Code.Formatter do
|
|||||||
remote_to_algebra(quoted, context, state)
|
remote_to_algebra(quoted, context, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# (left -> right)
|
||||||
|
defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do
|
||||||
|
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
|
||||||
|
end
|
||||||
|
|
||||||
# [keyword: :list] (inner part)
|
# [keyword: :list] (inner part)
|
||||||
# %{:foo => :bar} (inner part)
|
# %{:foo => :bar} (inner part)
|
||||||
defp quoted_to_algebra(list, context, state) when is_list(list) do
|
defp quoted_to_algebra(list, context, state) when is_list(list) do
|
||||||
@@ -605,12 +515,19 @@ defmodule Code.Formatter do
|
|||||||
if keyword_key?(left_arg) do
|
if keyword_key?(left_arg) do
|
||||||
{left, state} =
|
{left, state} =
|
||||||
case left_arg do
|
case left_arg do
|
||||||
{:__block__, _, [atom]} when is_atom(atom) ->
|
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||||
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
|
{:__block__, _, [:"..//"]} ->
|
||||||
|
{string(~S{"..//":}), state}
|
||||||
|
|
||||||
{formatted
|
{:__block__, _, [atom]} when is_atom(atom) ->
|
||||||
|> string()
|
key =
|
||||||
|> color_doc(:atom, state.inspect_opts), state}
|
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
|
||||||
|
IO.iodata_to_binary([Atom.to_string(atom), ?:])
|
||||||
|
else
|
||||||
|
IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:])
|
||||||
|
end
|
||||||
|
|
||||||
|
{string(key) |> color(:atom, state.inspect_opts), state}
|
||||||
|
|
||||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
|
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
|
||||||
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
|
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
|
||||||
@@ -626,22 +543,16 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
doc =
|
doc =
|
||||||
concat(
|
with_next_break_fits(next_break_fits?(right_arg, state), right, fn right ->
|
||||||
group(left),
|
concat(group(left), group(nest(glue(op, group(right)), 2, :break)))
|
||||||
with_next_break_fits(next_break_fits?(right_arg, state), right, fn right ->
|
end)
|
||||||
nest(glue(op, right), 2, :break)
|
|
||||||
end)
|
|
||||||
)
|
|
||||||
|
|
||||||
{doc, state}
|
{doc, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# #PID's and #Ref's may appear on regular AST
|
# #PID's and #Ref's may appear on regular AST
|
||||||
# Other foreign structures, such as maps and structs,
|
|
||||||
# may appear from Macro.to_string, so we stick a limit,
|
|
||||||
# although they won't be formatted accordingly.
|
|
||||||
defp quoted_to_algebra(unknown, _context, state) do
|
defp quoted_to_algebra(unknown, _context, state) do
|
||||||
{inspect(unknown, printable_limit: :infinity), state}
|
{inspect(unknown), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
## Blocks
|
## Blocks
|
||||||
@@ -711,12 +622,12 @@ defmodule Code.Formatter do
|
|||||||
Atom.to_string(op)
|
Atom.to_string(op)
|
||||||
end
|
end
|
||||||
|
|
||||||
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
|
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
|
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
|
||||||
with [left, right] <- args,
|
with [left, right] <- args,
|
||||||
{_, _} <- augmented_binary_op(fun) do
|
{_, _} <- Code.Identifier.binary_op(fun) do
|
||||||
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
|
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
@@ -743,7 +654,7 @@ defmodule Code.Formatter do
|
|||||||
|
|
||||||
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
|
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
|
||||||
when op in @right_new_line_before_binary_operators do
|
when op in @right_new_line_before_binary_operators do
|
||||||
op_info = augmented_binary_op(op)
|
op_info = Code.Identifier.binary_op(op)
|
||||||
op_string = op_string <> " "
|
op_string = op_string <> " "
|
||||||
left_context = left_op_context(context)
|
left_context = left_op_context(context)
|
||||||
right_context = right_op_context(context)
|
right_context = right_op_context(context)
|
||||||
@@ -780,7 +691,7 @@ defmodule Code.Formatter do
|
|||||||
|
|
||||||
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
|
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
|
||||||
when op in @pipeline_operators do
|
when op in @pipeline_operators do
|
||||||
op_info = augmented_binary_op(op)
|
op_info = Code.Identifier.binary_op(op)
|
||||||
left_context = left_op_context(context)
|
left_context = left_op_context(context)
|
||||||
right_context = right_op_context(context)
|
right_context = right_op_context(context)
|
||||||
max_line = line(meta)
|
max_line = line(meta)
|
||||||
@@ -794,7 +705,7 @@ defmodule Code.Formatter do
|
|||||||
{{doc, @empty, 1}, state}
|
{{doc, @empty, 1}, state}
|
||||||
|
|
||||||
{{op, context}, arg}, _args, state ->
|
{{op, context}, arg}, _args, state ->
|
||||||
op_info = augmented_binary_op(op)
|
op_info = Code.Identifier.binary_op(op)
|
||||||
op_string = Atom.to_string(op) <> " "
|
op_string = Atom.to_string(op) <> " "
|
||||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
|
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
|
||||||
{{concat(op_string, doc), @empty, 1}, state}
|
{{concat(op_string, doc), @empty, 1}, state}
|
||||||
@@ -804,7 +715,7 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
|
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
|
||||||
op_info = augmented_binary_op(op)
|
op_info = Code.Identifier.binary_op(op)
|
||||||
left_context = left_op_context(context)
|
left_context = left_op_context(context)
|
||||||
right_context = right_op_context(context)
|
right_context = right_op_context(context)
|
||||||
|
|
||||||
@@ -814,13 +725,15 @@ defmodule Code.Formatter do
|
|||||||
{right, state} =
|
{right, state} =
|
||||||
binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0)
|
binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0)
|
||||||
|
|
||||||
{op_string, right} =
|
doc =
|
||||||
cond do
|
cond do
|
||||||
op in @no_space_binary_operators ->
|
op in @no_space_binary_operators ->
|
||||||
{op_string, group(right)}
|
op_doc = color(op_string, :operator, state.inspect_opts)
|
||||||
|
concat(concat(group(left), op_doc), group(right))
|
||||||
|
|
||||||
op in @no_newline_binary_operators ->
|
op in @no_newline_binary_operators ->
|
||||||
{" " <> op_string <> " ", group(right)}
|
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
|
||||||
|
concat(concat(group(left), op_doc), group(right))
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
eol? = eol?(meta, state)
|
eol? = eol?(meta, state)
|
||||||
@@ -828,15 +741,14 @@ defmodule Code.Formatter do
|
|||||||
next_break_fits? =
|
next_break_fits? =
|
||||||
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
|
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
|
||||||
|
|
||||||
{" " <> op_string,
|
with_next_break_fits(next_break_fits?, right, fn right ->
|
||||||
with_next_break_fits(next_break_fits?, right, fn right ->
|
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
|
||||||
right = nest(concat(break(), right), nesting, :break)
|
right = nest(glue(op_doc, group(right)), nesting, :break)
|
||||||
if eol?, do: force_unfit(right), else: right
|
right = if eol?, do: force_unfit(right), else: right
|
||||||
end)}
|
concat(group(left), group(right))
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
op_doc = color_doc(op_string, :operator, state.inspect_opts)
|
|
||||||
doc = concat(concat(group(left), op_doc), group(right))
|
|
||||||
{doc, state}
|
{doc, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -858,54 +770,23 @@ defmodule Code.Formatter do
|
|||||||
{wrap_in_parens(doc), state}
|
{wrap_in_parens(doc), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# |> var
|
|
||||||
# |> var()
|
|
||||||
defp binary_operand_to_algebra(
|
|
||||||
{var, meta, var_context},
|
|
||||||
context,
|
|
||||||
%{migrate_call_parens_on_pipe: true} = state,
|
|
||||||
:|>,
|
|
||||||
_parent_info,
|
|
||||||
:right,
|
|
||||||
_nesting
|
|
||||||
)
|
|
||||||
when is_atom(var) and is_atom(var_context) do
|
|
||||||
operand = {var, meta, []}
|
|
||||||
quoted_to_algebra(operand, context, state)
|
|
||||||
end
|
|
||||||
|
|
||||||
# |> var.fun
|
|
||||||
# |> var.fun()
|
|
||||||
defp binary_operand_to_algebra(
|
|
||||||
{{:., _, [_, fun]} = call, meta, []},
|
|
||||||
context,
|
|
||||||
%{migrate_call_parens_on_pipe: true} = state,
|
|
||||||
:|>,
|
|
||||||
_parent_info,
|
|
||||||
:right,
|
|
||||||
_nesting
|
|
||||||
)
|
|
||||||
when is_atom(fun) do
|
|
||||||
meta = Keyword.put_new_lazy(meta, :closing, fn -> [line: meta[:line]] end)
|
|
||||||
quoted_to_algebra({call, meta, []}, context, state)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do
|
defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do
|
||||||
{parent_assoc, parent_prec} = parent_info
|
{parent_assoc, parent_prec} = parent_info
|
||||||
|
|
||||||
with {op, meta, [left, right]} <- operand,
|
with {op, meta, [left, right]} <- operand,
|
||||||
op_info = augmented_binary_op(op),
|
op_info = Code.Identifier.binary_op(op),
|
||||||
{_assoc, prec} <- op_info do
|
{_assoc, prec} <- op_info do
|
||||||
op_string = Atom.to_string(op)
|
op_string = Atom.to_string(op)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
# If we have the same operator and it is in the correct side,
|
# If the operator has the same precedence as the parent and is on
|
||||||
# we don't add parens unless it is explicitly required.
|
# the correct side, we respect the nesting rule to avoid multiple
|
||||||
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
|
# nestings. This only applies for left associativity or same operator.
|
||||||
|
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
|
||||||
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
|
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
|
||||||
|
|
||||||
# If the operator requires parens (most of them do) or we are mixing logical operators
|
# If the parent requires parens or the precedence is inverted or
|
||||||
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
|
# it is in the wrong side, then we *need* parenthesis.
|
||||||
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
|
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
|
||||||
(op in @required_parens_logical_binary_operands and
|
(op in @required_parens_logical_binary_operands and
|
||||||
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
|
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
|
||||||
@@ -1062,7 +943,7 @@ defmodule Code.Formatter do
|
|||||||
)
|
)
|
||||||
when is_atom(fun) and is_integer(arity) do
|
when is_atom(fun) and is_integer(arity) do
|
||||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||||
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|
fun = Macro.inspect_atom(:remote_call, fun)
|
||||||
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
|
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1109,9 +990,7 @@ defmodule Code.Formatter do
|
|||||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||||
|
|
||||||
fun_doc =
|
fun_doc =
|
||||||
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
|
||||||
|> string()
|
|
||||||
|> color_doc(:call, state.inspect_opts)
|
|
||||||
|
|
||||||
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
|
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
|
||||||
|
|
||||||
@@ -1179,7 +1058,7 @@ defmodule Code.Formatter do
|
|||||||
fun
|
fun
|
||||||
|> Atom.to_string()
|
|> Atom.to_string()
|
||||||
|> string()
|
|> string()
|
||||||
|> color_doc(:call, state.inspect_opts)
|
|> color(:call, state.inspect_opts)
|
||||||
|> concat(call_doc)
|
|> concat(call_doc)
|
||||||
|
|
||||||
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
|
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
|
||||||
@@ -1273,7 +1152,7 @@ defmodule Code.Formatter do
|
|||||||
args_doc =
|
args_doc =
|
||||||
if skip_parens? do
|
if skip_parens? do
|
||||||
left_doc
|
left_doc
|
||||||
|> concat(group(right_doc, :optimistic))
|
|> concat(next_break_fits(group(right_doc, :inherit), :enabled))
|
||||||
|> nest(:cursor, :break)
|
|> nest(:cursor, :break)
|
||||||
else
|
else
|
||||||
right_doc =
|
right_doc =
|
||||||
@@ -1281,7 +1160,8 @@ defmodule Code.Formatter do
|
|||||||
|> nest(2, :break)
|
|> nest(2, :break)
|
||||||
|> concat(break(""))
|
|> concat(break(""))
|
||||||
|> concat(")")
|
|> concat(")")
|
||||||
|> group(:optimistic)
|
|> group(:inherit)
|
||||||
|
|> next_break_fits(:enabled)
|
||||||
|
|
||||||
concat(nest(left_doc, 2, :break), right_doc)
|
concat(nest(left_doc, 2, :break), right_doc)
|
||||||
end
|
end
|
||||||
@@ -1324,11 +1204,13 @@ defmodule Code.Formatter do
|
|||||||
|> concat(args_doc)
|
|> concat(args_doc)
|
||||||
|> nest(2)
|
|> nest(2)
|
||||||
|> concat(extra)
|
|> concat(extra)
|
||||||
|
|> group()
|
||||||
|
|
||||||
skip_parens? ->
|
skip_parens? ->
|
||||||
" "
|
" "
|
||||||
|> concat(args_doc)
|
|> concat(args_doc)
|
||||||
|> concat(extra)
|
|> concat(extra)
|
||||||
|
|> group()
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
"("
|
"("
|
||||||
@@ -1336,12 +1218,13 @@ defmodule Code.Formatter do
|
|||||||
|> nest(2, :break)
|
|> nest(2, :break)
|
||||||
|> concat(args_doc)
|
|> concat(args_doc)
|
||||||
|> concat(extra)
|
|> concat(extra)
|
||||||
|
|> group()
|
||||||
end
|
end
|
||||||
|
|
||||||
if next_break_fits? do
|
if next_break_fits? do
|
||||||
{group(doc, :pessimistic), state}
|
{next_break_fits(doc, :disabled), state}
|
||||||
else
|
else
|
||||||
{group(doc), state}
|
{doc, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1465,7 +1348,7 @@ defmodule Code.Formatter do
|
|||||||
metadata = [
|
metadata = [
|
||||||
file: state.file,
|
file: state.file,
|
||||||
line: meta[:line],
|
line: meta[:line],
|
||||||
sigil: String.to_unsafe_atom(name),
|
sigil: String.to_atom(name),
|
||||||
modifiers: modifiers,
|
modifiers: modifiers,
|
||||||
opening_delimiter: opening_delimiter
|
opening_delimiter: opening_delimiter
|
||||||
]
|
]
|
||||||
@@ -1538,7 +1421,7 @@ defmodule Code.Formatter do
|
|||||||
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
|
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
|
||||||
|
|
||||||
{spec, state} =
|
{spec, state} =
|
||||||
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
|
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
|
||||||
|
|
||||||
spec = wrap_in_parens_if_inspected_atom(spec)
|
spec = wrap_in_parens_if_inspected_atom(spec)
|
||||||
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
|
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
|
||||||
@@ -1616,8 +1499,8 @@ defmodule Code.Formatter do
|
|||||||
{args_doc, _join, state} =
|
{args_doc, _join, state} =
|
||||||
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
||||||
|
|
||||||
left_bracket = color_doc("[", :list, state.inspect_opts)
|
left_bracket = color("[", :list, state.inspect_opts)
|
||||||
right_bracket = color_doc("]", :list, state.inspect_opts)
|
right_bracket = color("]", :list, state.inspect_opts)
|
||||||
|
|
||||||
{surround(left_bracket, args_doc, right_bracket), state}
|
{surround(left_bracket, args_doc, right_bracket), state}
|
||||||
end
|
end
|
||||||
@@ -1649,8 +1532,8 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp do_map_to_algebra(name_doc, args_doc, state) do
|
defp do_map_to_algebra(name_doc, args_doc, state) do
|
||||||
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
|
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
|
||||||
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
|
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp tuple_to_algebra(meta, args, join, state) do
|
defp tuple_to_algebra(meta, args, join, state) do
|
||||||
@@ -1660,8 +1543,8 @@ defmodule Code.Formatter do
|
|||||||
{args_doc, join, state} =
|
{args_doc, join, state} =
|
||||||
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
||||||
|
|
||||||
left_bracket = color_doc("{", :tuple, state.inspect_opts)
|
left_bracket = color("{", :tuple, state.inspect_opts)
|
||||||
right_bracket = color_doc("}", :tuple, state.inspect_opts)
|
right_bracket = color("}", :tuple, state.inspect_opts)
|
||||||
|
|
||||||
if join == :flex_break do
|
if join == :flex_break do
|
||||||
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
|
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
|
||||||
@@ -1671,11 +1554,16 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
|
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
|
||||||
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
|
Atom.to_string(atom) |> color(:boolean, inspect_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp atom_to_algebra(nil, _, inspect_opts) do
|
defp atom_to_algebra(nil, _, inspect_opts) do
|
||||||
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
|
Atom.to_string(nil) |> color(nil, inspect_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||||
|
defp atom_to_algebra(:"..//", _, inspect_opts) do
|
||||||
|
string(":\"..//\"") |> color(:atom, inspect_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp atom_to_algebra(:\\, meta, inspect_opts) do
|
defp atom_to_algebra(:\\, meta, inspect_opts) do
|
||||||
@@ -1688,7 +1576,7 @@ defmodule Code.Formatter do
|
|||||||
_ -> ":\\\\"
|
_ -> ":\\\\"
|
||||||
end
|
end
|
||||||
|
|
||||||
string(string) |> color_doc(:atom, inspect_opts)
|
string(string) |> color(:atom, inspect_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp atom_to_algebra(atom, _, inspect_opts) do
|
defp atom_to_algebra(atom, _, inspect_opts) do
|
||||||
@@ -1701,10 +1589,10 @@ defmodule Code.Formatter do
|
|||||||
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
|
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
|
||||||
end
|
end
|
||||||
|
|
||||||
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
|
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp integer_to_algebra(text, inspect_opts) do
|
defp integer_to_algebra(text, inspect_otps) do
|
||||||
case text do
|
case text do
|
||||||
<<?0, ?x, rest::binary>> ->
|
<<?0, ?x, rest::binary>> ->
|
||||||
"0x" <> String.upcase(rest)
|
"0x" <> String.upcase(rest)
|
||||||
@@ -1718,15 +1606,15 @@ defmodule Code.Formatter do
|
|||||||
decimal ->
|
decimal ->
|
||||||
insert_underscores(decimal)
|
insert_underscores(decimal)
|
||||||
end
|
end
|
||||||
|> color_doc(:number, inspect_opts)
|
|> color(:number, inspect_otps)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp float_to_algebra(text, inspect_opts) do
|
defp float_to_algebra(text, inspect_otps) do
|
||||||
[int_part, decimal_part] = :binary.split(text, ".")
|
[int_part, decimal_part] = :binary.split(text, ".")
|
||||||
decimal_part = String.downcase(decimal_part)
|
decimal_part = String.downcase(decimal_part)
|
||||||
|
|
||||||
string = insert_underscores(int_part) <> "." <> decimal_part
|
string = insert_underscores(int_part) <> "." <> decimal_part
|
||||||
color_doc(string, :number, inspect_opts)
|
color(string, :number, inspect_otps)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp insert_underscores("-" <> digits) do
|
defp insert_underscores("-" <> digits) do
|
||||||
@@ -1734,30 +1622,25 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp insert_underscores(digits) do
|
defp insert_underscores(digits) do
|
||||||
byte_size = byte_size(digits)
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
digits =~ "_" ->
|
digits =~ "_" ->
|
||||||
digits
|
digits
|
||||||
|
|
||||||
byte_size >= 6 ->
|
byte_size(digits) >= 6 ->
|
||||||
offset = rem(byte_size, 3)
|
digits
|
||||||
{prefix, rest} = String.split_at(digits, offset)
|
|> String.to_charlist()
|
||||||
do_insert_underscores(prefix, rest)
|
|> Enum.reverse()
|
||||||
|
|> Enum.chunk_every(3)
|
||||||
|
|> Enum.intersperse(~c"_")
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> List.to_string()
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
digits
|
digits
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_insert_underscores(acc, ""), do: acc
|
|
||||||
|
|
||||||
defp do_insert_underscores("", <<next::binary-3, rest::binary>>),
|
|
||||||
do: do_insert_underscores(next, rest)
|
|
||||||
|
|
||||||
defp do_insert_underscores(acc, <<next::binary-3, rest::binary>>),
|
|
||||||
do: do_insert_underscores(<<acc::binary, "_", next::binary>>, rest)
|
|
||||||
|
|
||||||
defp escape_heredoc(string, escape) do
|
defp escape_heredoc(string, escape) do
|
||||||
string = String.replace(string, escape, "\\" <> escape)
|
string = String.replace(string, escape, "\\" <> escape)
|
||||||
heredoc_to_algebra(["" | String.split(string, "\n")])
|
heredoc_to_algebra(["" | String.split(string, "\n")])
|
||||||
@@ -1795,7 +1678,6 @@ defmodule Code.Formatter do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp heredoc_line(["", _ | _]), do: nest(line(), :reset)
|
defp heredoc_line(["", _ | _]), do: nest(line(), :reset)
|
||||||
defp heredoc_line(["\r", _ | _]), do: nest(line(), :reset)
|
|
||||||
defp heredoc_line(_), do: line()
|
defp heredoc_line(_), do: line()
|
||||||
|
|
||||||
defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do
|
defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do
|
||||||
@@ -1807,17 +1689,10 @@ defmodule Code.Formatter do
|
|||||||
|
|
||||||
doc =
|
doc =
|
||||||
case args do
|
case args do
|
||||||
[_ | _] ->
|
[_ | _] -> concat_to_last_group(doc, ",")
|
||||||
concat_to_last_group(doc, ",")
|
[] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",")
|
||||||
|
[] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled)
|
||||||
[] when last_arg_mode == :force_comma ->
|
[] when last_arg_mode == :none -> doc
|
||||||
concat_to_last_group(doc, ",")
|
|
||||||
|
|
||||||
[] when last_arg_mode == :next_break_fits ->
|
|
||||||
doc |> ungroup_if_group() |> group(:optimistic)
|
|
||||||
|
|
||||||
[] when last_arg_mode == :none ->
|
|
||||||
doc
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{{doc, @empty, 1}, state}
|
{{doc, @empty, 1}, state}
|
||||||
@@ -2075,14 +1950,6 @@ defmodule Code.Formatter do
|
|||||||
# fn a, b, c when d -> e end
|
# fn a, b, c when d -> e end
|
||||||
defp clause_args_to_algebra([{:when, meta, args}], state) do
|
defp clause_args_to_algebra([{:when, meta, args}], state) do
|
||||||
{args, right} = split_last(args)
|
{args, right} = split_last(args)
|
||||||
|
|
||||||
# If there are any keywords, wrap them in lists
|
|
||||||
args =
|
|
||||||
Enum.map(args, fn
|
|
||||||
[_ | _] = keyword -> {:__block__, [], [keyword]}
|
|
||||||
other -> other
|
|
||||||
end)
|
|
||||||
|
|
||||||
left = {{:special, :clause_args}, meta, [args]}
|
left = {{:special, :clause_args}, meta, [args]}
|
||||||
binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state)
|
binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state)
|
||||||
end
|
end
|
||||||
@@ -2312,15 +2179,10 @@ defmodule Code.Formatter do
|
|||||||
unary_operator?(quoted) or binary_operator?(quoted)
|
unary_operator?(quoted) or binary_operator?(quoted)
|
||||||
end
|
end
|
||||||
|
|
||||||
# We convert ..// into two operators for simplicity,
|
|
||||||
# so we need to augment the binary table.
|
|
||||||
defp augmented_binary_op(:"//"), do: {:right, 190}
|
|
||||||
defp augmented_binary_op(op), do: Code.Identifier.binary_op(op)
|
|
||||||
|
|
||||||
defp binary_operator?(quoted) do
|
defp binary_operator?(quoted) do
|
||||||
case quoted do
|
case quoted do
|
||||||
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
|
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
|
||||||
{op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error
|
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2335,14 +2197,11 @@ defmodule Code.Formatter do
|
|||||||
defp with_next_break_fits(condition, doc, fun) do
|
defp with_next_break_fits(condition, doc, fun) do
|
||||||
if condition do
|
if condition do
|
||||||
doc
|
doc
|
||||||
|> group(:optimistic)
|
|> next_break_fits(:enabled)
|
||||||
|> fun.()
|
|> fun.()
|
||||||
|> group(:pessimistic)
|
|> next_break_fits(:disabled)
|
||||||
else
|
else
|
||||||
doc
|
fun.(doc)
|
||||||
|> group()
|
|
||||||
|> fun.()
|
|
||||||
|> group()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2359,7 +2218,6 @@ defmodule Code.Formatter do
|
|||||||
(not interpolated?(entries) and eol_or_comments?(meta, state))
|
(not interpolated?(entries) and eol_or_comments?(meta, state))
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
|
|
||||||
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
|
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
|
||||||
meta[:delimiter] == ~s[''']
|
meta[:delimiter] == ~s[''']
|
||||||
end
|
end
|
||||||
@@ -2501,24 +2359,20 @@ defmodule Code.Formatter do
|
|||||||
meta[:closing][:line] || @min_line
|
meta[:closing][:line] || @min_line
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_atom(string, char) do
|
|
||||||
String.replace(string, <<char>>, <<?\\, char>>)
|
|
||||||
end
|
|
||||||
|
|
||||||
## Algebra helpers
|
## Algebra helpers
|
||||||
|
|
||||||
# Relying on the inner document is brittle and error prone.
|
# Relying on the inner document is brittle and error prone.
|
||||||
# It would be best if we had a mechanism to apply this.
|
# It would be best if we had a mechanism to apply this.
|
||||||
defp concat_to_last_group([left | right], concat) do
|
defp concat_to_last_group({:doc_cons, left, right}, concat) do
|
||||||
[left | concat_to_last_group(right, concat)]
|
{:doc_cons, left, concat_to_last_group(right, concat)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp concat_to_last_group({:doc_group, group, mode}, concat) do
|
defp concat_to_last_group({:doc_group, group, mode}, concat) do
|
||||||
{:doc_group, concat(group, concat), mode}
|
{:doc_group, {:doc_cons, group, concat}, mode}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp concat_to_last_group(other, concat) do
|
defp concat_to_last_group(other, concat) do
|
||||||
concat(other, concat)
|
{:doc_cons, other, concat}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ungroup_if_group({:doc_group, group, _mode}), do: group
|
defp ungroup_if_group({:doc_group, group, _mode}), do: group
|
||||||
@@ -2549,65 +2403,19 @@ defmodule Code.Formatter do
|
|||||||
{left, right}
|
{left, right}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_charlist_quotes(:heredoc, state) do
|
defp get_charlist_quotes(_heredoc = false, state) do
|
||||||
if state.migrate_charlists_as_sigils do
|
if state.normalize_charlists_as_sigils do
|
||||||
|
{@sigil_c, @double_quote}
|
||||||
|
else
|
||||||
|
{@single_quote, @single_quote}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_charlist_quotes(_heredoc = true, state) do
|
||||||
|
if state.normalize_charlists_as_sigils do
|
||||||
{@sigil_c_heredoc, @double_heredoc}
|
{@sigil_c_heredoc, @double_heredoc}
|
||||||
else
|
else
|
||||||
{@single_heredoc, @single_heredoc}
|
{@single_heredoc, @single_heredoc}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_charlist_quotes({:regular, chunks}, state) do
|
|
||||||
cond do
|
|
||||||
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
|
|
||||||
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
|
|
||||||
true -> {@sigil_c_double, @double_quote}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp has_double_quote?(chunk) do
|
|
||||||
is_binary(chunk) and chunk =~ @double_quote
|
|
||||||
end
|
|
||||||
|
|
||||||
# Migration rewrites
|
|
||||||
|
|
||||||
@bool_operators [
|
|
||||||
:>,
|
|
||||||
:>=,
|
|
||||||
:<,
|
|
||||||
:<=,
|
|
||||||
:in
|
|
||||||
]
|
|
||||||
@guards [
|
|
||||||
:is_atom,
|
|
||||||
:is_boolean,
|
|
||||||
:is_nil,
|
|
||||||
:is_number,
|
|
||||||
:is_integer,
|
|
||||||
:is_float,
|
|
||||||
:is_binary,
|
|
||||||
:is_map,
|
|
||||||
:is_struct,
|
|
||||||
:is_non_struct_map,
|
|
||||||
:is_exception,
|
|
||||||
:is_list,
|
|
||||||
:is_tuple,
|
|
||||||
:is_function,
|
|
||||||
:is_reference,
|
|
||||||
:is_pid,
|
|
||||||
:is_port
|
|
||||||
]
|
|
||||||
|
|
||||||
defp negate_condition(condition) do
|
|
||||||
case condition do
|
|
||||||
{neg, _, [condition]} when neg in [:!, :not] -> condition
|
|
||||||
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
|
|
||||||
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
|
|
||||||
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
|
|
||||||
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
|
|
||||||
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
|
|
||||||
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
|
|
||||||
_ -> {:!, [], [condition]}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
+62
-353
@@ -1,6 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
defmodule Code.Fragment do
|
defmodule Code.Fragment do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This module provides conveniences for analyzing fragments of
|
This module provides conveniences for analyzing fragments of
|
||||||
@@ -11,61 +8,6 @@ defmodule Code.Fragment do
|
|||||||
|
|
||||||
@type position :: {line :: pos_integer(), column :: pos_integer()}
|
@type position :: {line :: pos_integer(), column :: pos_integer()}
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Options for cursor context functions.
|
|
||||||
|
|
||||||
Currently, these options are not used but reserved for future extensibility.
|
|
||||||
"""
|
|
||||||
@type cursor_opts :: []
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Options for converting code fragments to quoted expressions.
|
|
||||||
"""
|
|
||||||
@type container_cursor_to_quoted_opts :: [
|
|
||||||
file: String.t(),
|
|
||||||
line: pos_integer(),
|
|
||||||
column: pos_integer(),
|
|
||||||
columns: boolean(),
|
|
||||||
token_metadata: boolean(),
|
|
||||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
|
||||||
preserve_sigils: boolean(),
|
|
||||||
trailing_fragment: String.t()
|
|
||||||
]
|
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Returns the list of lines in the given string, preserving their line endings.
|
|
||||||
|
|
||||||
Only the line endings recognized by the Elixir compiler are
|
|
||||||
considered, namely `\r\n` and `\n`. If you would like to retrieve
|
|
||||||
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
|
|
||||||
["foo\r\n", "bar\r\n", "baz"]
|
|
||||||
|
|
||||||
iex> Code.Fragment.lines("foo\nbar\nbaz")
|
|
||||||
["foo\n", "bar\n", "baz"]
|
|
||||||
|
|
||||||
iex> Code.Fragment.lines("")
|
|
||||||
[""]
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.19.0"
|
|
||||||
@spec lines(String.t()) :: [String.t()]
|
|
||||||
def lines(string) do
|
|
||||||
lines(string, <<>>)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp lines(<<?\n, rest::binary>>, acc),
|
|
||||||
do: [<<acc::binary, ?\n>> | lines(rest, <<>>)]
|
|
||||||
|
|
||||||
defp lines(<<char, rest::binary>>, acc),
|
|
||||||
do: lines(rest, <<acc::binary, char>>)
|
|
||||||
|
|
||||||
defp lines(<<>>, acc),
|
|
||||||
do: [acc]
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Receives a string and returns the cursor context.
|
Receives a string and returns the cursor context.
|
||||||
|
|
||||||
@@ -89,7 +31,7 @@ defmodule Code.Fragment do
|
|||||||
:expr
|
:expr
|
||||||
|
|
||||||
iex> Code.Fragment.cursor_context("hello_wor")
|
iex> Code.Fragment.cursor_context("hello_wor")
|
||||||
{:local_or_var, ~c"hello_wor"}
|
{:local_or_var, 'hello_wor'}
|
||||||
|
|
||||||
## Return values
|
## Return values
|
||||||
|
|
||||||
@@ -101,9 +43,6 @@ defmodule Code.Fragment do
|
|||||||
or `{:local_or_var, charlist}` and `charlist` is a static part
|
or `{:local_or_var, charlist}` and `charlist` is a static part
|
||||||
Examples are `__MODULE__.Submodule` or `@hello.Submodule`
|
Examples are `__MODULE__.Submodule` or `@hello.Submodule`
|
||||||
|
|
||||||
* `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
|
|
||||||
catch, else, rescue) or a binary operator
|
|
||||||
|
|
||||||
* `{:dot, inside_dot, charlist}` - the context is a dot
|
* `{:dot, inside_dot, charlist}` - the context is a dot
|
||||||
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
|
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
|
||||||
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
|
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
|
||||||
@@ -139,12 +78,6 @@ defmodule Code.Fragment do
|
|||||||
* `{:local_call, charlist}` - the context is a local (import or local)
|
* `{:local_call, charlist}` - the context is a local (import or local)
|
||||||
call, such as `hello_world(` and `hello_world `
|
call, such as `hello_world(` and `hello_world `
|
||||||
|
|
||||||
* `{:anonymous_call, inside_caller}` - the context is an anonymous
|
|
||||||
call, such as `fun.(` and `@fun.(`.
|
|
||||||
|
|
||||||
* `{:capture_arg, charlist}` - the context is a capture argument,
|
|
||||||
such as `&1`
|
|
||||||
|
|
||||||
* `{:module_attribute, charlist}` - the context is a module attribute,
|
* `{:module_attribute, charlist}` - the context is a module attribute,
|
||||||
such as `@hello_wor`
|
such as `@hello_wor`
|
||||||
|
|
||||||
@@ -162,8 +95,8 @@ defmodule Code.Fragment do
|
|||||||
* `:none` - no context possible
|
* `:none` - no context possible
|
||||||
|
|
||||||
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
|
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
|
||||||
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as
|
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
|
||||||
`~>` and `~>>`, are returned as :operator contexts
|
`~>` and `~>>`
|
||||||
|
|
||||||
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
|
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
|
||||||
`inside_struct` can either be a `charlist` in case of a static alias or an
|
`inside_struct` can either be a `charlist` in case of a static alias or an
|
||||||
@@ -197,10 +130,9 @@ defmodule Code.Fragment do
|
|||||||
references, and more.
|
references, and more.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.13.0"
|
@doc since: "1.13.0"
|
||||||
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
|
@spec cursor_context(List.Chars.t(), keyword()) ::
|
||||||
{:alias, charlist}
|
{:alias, charlist}
|
||||||
| {:alias, inside_alias, charlist}
|
| {:alias, inside_alias, charlist}
|
||||||
| {:block_keyword_or_binary_operator, charlist}
|
|
||||||
| {:dot, inside_dot, charlist}
|
| {:dot, inside_dot, charlist}
|
||||||
| {:dot_arity, inside_dot, charlist}
|
| {:dot_arity, inside_dot, charlist}
|
||||||
| {:dot_call, inside_dot, charlist}
|
| {:dot_call, inside_dot, charlist}
|
||||||
@@ -208,8 +140,6 @@ defmodule Code.Fragment do
|
|||||||
| {:local_or_var, charlist}
|
| {:local_or_var, charlist}
|
||||||
| {:local_arity, charlist}
|
| {:local_arity, charlist}
|
||||||
| {:local_call, charlist}
|
| {:local_call, charlist}
|
||||||
| {:anonymous_call, inside_caller}
|
|
||||||
| {:capture_arg, charlist}
|
|
||||||
| {:module_attribute, charlist}
|
| {:module_attribute, charlist}
|
||||||
| {:operator, charlist}
|
| {:operator, charlist}
|
||||||
| {:operator_arity, charlist}
|
| {:operator_arity, charlist}
|
||||||
@@ -234,8 +164,7 @@ defmodule Code.Fragment do
|
|||||||
| {:alias, inside_alias, charlist}
|
| {:alias, inside_alias, charlist}
|
||||||
| {:local_or_var, charlist}
|
| {:local_or_var, charlist}
|
||||||
| {:module_attribute, charlist}
|
| {:module_attribute, charlist}
|
||||||
| {:dot, inside_dot, charlist},
|
| {:dot, inside_dot, charlist}
|
||||||
inside_caller: {:var, charlist} | {:module_attribute, charlist}
|
|
||||||
def cursor_context(fragment, opts \\ [])
|
def cursor_context(fragment, opts \\ [])
|
||||||
|
|
||||||
def cursor_context(fragment, opts)
|
def cursor_context(fragment, opts)
|
||||||
@@ -251,15 +180,15 @@ defmodule Code.Fragment do
|
|||||||
cursor_context(to_charlist(other), opts)
|
cursor_context(to_charlist(other), opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@operators ~c"\\<>+-*/:=|&~^%!$"
|
@operators ~c"\\<>+-*/:=|&~^%!"
|
||||||
@starting_punctuation ~c",([{;"
|
@starter_punctuation ~c",([{;"
|
||||||
@closing_punctuation ~c")]}\"'"
|
@non_starter_punctuation ~c")]}\"'.$"
|
||||||
@space ~c"\t\s"
|
@space ~c"\t\s"
|
||||||
@trailing_identifier ~c"?!"
|
@trailing_identifier ~c"?!"
|
||||||
@tilde_op_prefix ~c"<=~"
|
@tilde_op_prefix ~c"<=~"
|
||||||
|
|
||||||
@non_identifier @trailing_identifier ++
|
@non_identifier @trailing_identifier ++
|
||||||
@operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.]
|
@operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space
|
||||||
|
|
||||||
@textual_operators ~w(when not and or in)c
|
@textual_operators ~w(when not and or in)c
|
||||||
@keywords ~w(do end after else catch rescue fn true false nil)c
|
@keywords ~w(do end after else catch rescue fn true false nil)c
|
||||||
@@ -289,11 +218,11 @@ defmodule Code.Fragment do
|
|||||||
# A local arity definition
|
# A local arity definition
|
||||||
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
|
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
|
||||||
# Starting a new expression
|
# Starting a new expression
|
||||||
[h | _] when h in @starting_punctuation -> {:expr, 0}
|
[h | _] when h in @starter_punctuation -> {:expr, 0}
|
||||||
# It is keyword, binary operator, a local or remote call without parens
|
# It is a local or remote call without parens
|
||||||
rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces})
|
rest when spaces > 0 -> call_to_cursor_context({rest, spaces})
|
||||||
# It is an identifier
|
# It is an identifier
|
||||||
_ -> identifier_to_cursor_context(reverse, spaces, false)
|
_ -> identifier_to_cursor_context(reverse, 0, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -308,41 +237,21 @@ defmodule Code.Fragment do
|
|||||||
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
|
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
|
||||||
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
|
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
|
||||||
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
|
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
|
||||||
{{:sigil, _}, _} -> {:none, 0}
|
{_, _} -> {:none, 0}
|
||||||
{_, _} -> {{:operator, ~c"/"}, 1}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp call_to_cursor_context({reverse, spaces}) do
|
defp call_to_cursor_context({reverse, spaces}) do
|
||||||
with [?. | rest] <- reverse,
|
case identifier_to_cursor_context(reverse, spaces, true) do
|
||||||
{rest, spaces} = strip_spaces(rest, spaces),
|
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
|
||||||
[h | _] when h not in @non_identifier <- rest do
|
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
|
||||||
case identifier_to_cursor_context(rest, spaces, true) do
|
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
|
||||||
{{:local_or_var, acc}, count} -> {{:anonymous_call, {:var, acc}}, count + 1}
|
{_, _} -> {:none, 0}
|
||||||
{{:module_attribute, _} = attr, count} -> {{:anonymous_call, attr}, count + 1}
|
|
||||||
{_, _} -> {:none, 0}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_ ->
|
|
||||||
case identifier_to_cursor_context(reverse, spaces, true) do
|
|
||||||
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
|
|
||||||
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
|
|
||||||
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
|
|
||||||
{_, _} -> {:none, 0}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp closing_or_call_to_cursor_context({reverse, spaces}) do
|
|
||||||
if closing?(reverse) do
|
|
||||||
{{:block_keyword_or_binary_operator, ~c""}, 0}
|
|
||||||
else
|
|
||||||
call_to_cursor_context({reverse, spaces})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
|
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
|
||||||
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
|
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
|
||||||
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
|
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
|
||||||
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
|
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
|
||||||
|
|
||||||
@@ -392,42 +301,12 @@ defmodule Code.Fragment do
|
|||||||
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
|
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
|
||||||
dot(rest, count + 1, acc)
|
dot(rest, count + 1, acc)
|
||||||
|
|
||||||
{rest, rest_count} ->
|
_ ->
|
||||||
response =
|
{{:local_or_var, acc}, count}
|
||||||
if rest_count > count and closing?(rest),
|
|
||||||
do: :block_keyword_or_binary_operator,
|
|
||||||
else: :local_or_var
|
|
||||||
|
|
||||||
{{response, acc}, count}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:capture_arg, acc, count} ->
|
|
||||||
{{:capture_arg, acc}, count}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# If it is a closing punctuation
|
|
||||||
defp closing?([h | _]) when h in @closing_punctuation, do: true
|
|
||||||
# Closing bitstring (but deal with operators)
|
|
||||||
defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
|
|
||||||
# Keywords
|
|
||||||
defp closing?(rest) do
|
|
||||||
case split_non_identifier(rest, []) do
|
|
||||||
{~c"nil", _} -> true
|
|
||||||
{~c"true", _} -> true
|
|
||||||
{~c"false", _} -> true
|
|
||||||
{[digit | _], _} when digit in ?0..?9 -> true
|
|
||||||
{[upper | _], _} when upper in ?A..?Z -> true
|
|
||||||
{[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
|
|
||||||
{_, _} -> false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp split_non_identifier([h | t], acc) when h not in @non_identifier,
|
|
||||||
do: split_non_identifier(t, [h | acc])
|
|
||||||
|
|
||||||
defp split_non_identifier(rest, acc), do: {acc, rest}
|
|
||||||
|
|
||||||
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
|
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
|
||||||
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
|
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
|
||||||
defp identifier(rest, count), do: check_identifier(rest, count, [])
|
defp identifier(rest, count), do: check_identifier(rest, count, [])
|
||||||
@@ -467,14 +346,6 @@ defmodule Code.Fragment do
|
|||||||
:none
|
:none
|
||||||
end
|
end
|
||||||
|
|
||||||
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
|
|
||||||
if Enum.all?(acc, &(&1 in ?0..?9)) do
|
|
||||||
{:capture_arg, [?& | acc], count + 1}
|
|
||||||
else
|
|
||||||
tokenize_identifier(rest, count, acc)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp rest_identifier(rest, count, acc) do
|
defp rest_identifier(rest, count, acc) do
|
||||||
tokenize_identifier(rest, count, acc)
|
tokenize_identifier(rest, count, acc)
|
||||||
end
|
end
|
||||||
@@ -594,15 +465,15 @@ defmodule Code.Fragment do
|
|||||||
|
|
||||||
defp operator(rest, count, acc, _call_op?) do
|
defp operator(rest, count, acc, _call_op?) do
|
||||||
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
|
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
|
||||||
{:ok, _, _, _, [{:atom, _, _}], []} ->
|
{:ok, _, _, _, [{:atom, _, _}]} ->
|
||||||
{{:unquoted_atom, tl(acc)}, count}
|
{{:unquoted_atom, tl(acc)}, count}
|
||||||
|
|
||||||
{:ok, _, _, _, [{_, _, op}], []} ->
|
{:ok, _, _, _, [{_, _, op}]} ->
|
||||||
{rest, dot_count} = strip_spaces(rest, count)
|
{rest, dot_count} = strip_spaces(rest, count)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
|
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
|
||||||
{:none, 0}
|
:none
|
||||||
|
|
||||||
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
|
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
|
||||||
dot(tl(rest), dot_count + 1, acc)
|
dot(tl(rest), dot_count + 1, acc)
|
||||||
@@ -660,9 +531,9 @@ defmodule Code.Fragment do
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Code.Fragment.surround_context("foo", {1, 1})
|
iex> Code.Fragment.surround_context("foo", {1, 1})
|
||||||
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
|
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
|
||||||
|
|
||||||
## Differences from `cursor_context/2`
|
## Differences to `cursor_context/2`
|
||||||
|
|
||||||
Because `surround_context/3` attempts to capture complex expressions,
|
Because `surround_context/3` attempts to capture complex expressions,
|
||||||
it has some differences to `cursor_context/2`:
|
it has some differences to `cursor_context/2`:
|
||||||
@@ -675,13 +546,13 @@ defmodule Code.Fragment do
|
|||||||
`local_call`/`local_arity` and `local_or_var`, since the latter can
|
`local_call`/`local_arity` and `local_or_var`, since the latter can
|
||||||
be a local or variable
|
be a local or variable
|
||||||
|
|
||||||
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
|
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
|
||||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`)
|
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
|
||||||
|
|
||||||
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
|
* This function never returns empty sigils `{:sigil, ''}` or empty structs
|
||||||
`{:struct, ~c""}` as context
|
`{:struct, ''}` as context
|
||||||
|
|
||||||
* This function returns keywords as `{:keyword, ~c"do"}`
|
* This function returns keywords as `{:keyword, 'do'}`
|
||||||
|
|
||||||
* This function never returns `:expr`
|
* This function never returns `:expr`
|
||||||
|
|
||||||
@@ -689,7 +560,7 @@ defmodule Code.Fragment do
|
|||||||
of examples and their return values.
|
of examples and their return values.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.13.0"
|
@doc since: "1.13.0"
|
||||||
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
|
@spec surround_context(List.Chars.t(), position(), keyword()) ::
|
||||||
%{begin: position, end: position, context: context} | :none
|
%{begin: position, end: position, context: context} | :none
|
||||||
when context:
|
when context:
|
||||||
{:alias, charlist}
|
{:alias, charlist}
|
||||||
@@ -703,17 +574,14 @@ defmodule Code.Fragment do
|
|||||||
| {:sigil, charlist}
|
| {:sigil, charlist}
|
||||||
| {:struct, inside_struct}
|
| {:struct, inside_struct}
|
||||||
| {:unquoted_atom, charlist}
|
| {:unquoted_atom, charlist}
|
||||||
| {:keyword, charlist}
|
| {:keyword, charlist},
|
||||||
| {:key, charlist}
|
|
||||||
| {:capture_arg, charlist},
|
|
||||||
inside_dot:
|
inside_dot:
|
||||||
{:alias, charlist}
|
{:alias, charlist}
|
||||||
| {:alias, inside_alias, charlist}
|
| {:alias, inside_alias, charlist}
|
||||||
| {:dot, inside_dot, charlist}
|
| {:dot, inside_dot, charlist}
|
||||||
| {:module_attribute, charlist}
|
| {:module_attribute, charlist}
|
||||||
| {:unquoted_atom, charlist}
|
| {:unquoted_atom, charlist}
|
||||||
| {:var, charlist}
|
| {:var, charlist},
|
||||||
| :expr,
|
|
||||||
inside_alias:
|
inside_alias:
|
||||||
{:local_or_var, charlist}
|
{:local_or_var, charlist}
|
||||||
| {:module_attribute, charlist},
|
| {:module_attribute, charlist},
|
||||||
@@ -751,24 +619,15 @@ defmodule Code.Fragment do
|
|||||||
{reversed_pre, post} = adjust_position(reversed_pre, post)
|
{reversed_pre, post} = adjust_position(reversed_pre, post)
|
||||||
|
|
||||||
case take_identifier(post, []) do
|
case take_identifier(post, []) do
|
||||||
{_, [], _} ->
|
:none ->
|
||||||
maybe_operator(reversed_pre, post, line, opts)
|
maybe_operator(reversed_pre, post, line, opts)
|
||||||
|
|
||||||
{:identifier, reversed_post, rest} ->
|
{:identifier, reversed_post, rest} ->
|
||||||
{keyword_key?, rest} =
|
{rest, _} = strip_spaces(rest, 0)
|
||||||
case rest do
|
|
||||||
[?: | tail] when tail == [] or hd(tail) in @space ->
|
|
||||||
{true, rest}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{rest, _} = strip_spaces(rest, 0)
|
|
||||||
{false, rest}
|
|
||||||
end
|
|
||||||
|
|
||||||
reversed = reversed_post ++ reversed_pre
|
reversed = reversed_post ++ reversed_pre
|
||||||
|
|
||||||
case codepoint_cursor_context(reversed, opts) do
|
case codepoint_cursor_context(reversed, opts) do
|
||||||
{{:struct, acc}, offset} ->
|
{{:struct, acc}, offset} when acc != [] ->
|
||||||
build_surround({:struct, acc}, reversed, line, offset)
|
build_surround({:struct, acc}, reversed, line, offset)
|
||||||
|
|
||||||
{{:alias, acc}, offset} ->
|
{{:alias, acc}, offset} ->
|
||||||
@@ -780,9 +639,6 @@ defmodule Code.Fragment do
|
|||||||
{{:dot, _, [_ | _]} = dot, offset} ->
|
{{:dot, _, [_ | _]} = dot, offset} ->
|
||||||
build_surround(dot, reversed, line, offset)
|
build_surround(dot, reversed, line, offset)
|
||||||
|
|
||||||
{{:local_or_var, acc}, offset} when keyword_key? ->
|
|
||||||
build_surround({:key, acc}, reversed, line, offset)
|
|
||||||
|
|
||||||
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
|
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
|
||||||
build_surround({:local_call, acc}, reversed, line, offset)
|
build_surround({:local_call, acc}, reversed, line, offset)
|
||||||
|
|
||||||
@@ -798,12 +654,6 @@ defmodule Code.Fragment do
|
|||||||
{{:local_or_var, acc}, offset} ->
|
{{:local_or_var, acc}, offset} ->
|
||||||
build_surround({:local_or_var, acc}, reversed, line, offset)
|
build_surround({:local_or_var, acc}, reversed, line, offset)
|
||||||
|
|
||||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
|
|
||||||
build_surround({:operator, acc}, reversed, line, offset)
|
|
||||||
|
|
||||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
|
|
||||||
build_surround({:keyword, acc}, reversed, line, offset)
|
|
||||||
|
|
||||||
{{:module_attribute, ~c""}, offset} ->
|
{{:module_attribute, ~c""}, offset} ->
|
||||||
build_surround({:operator, ~c"@"}, reversed, line, offset)
|
build_surround({:operator, ~c"@"}, reversed, line, offset)
|
||||||
|
|
||||||
@@ -816,9 +666,6 @@ defmodule Code.Fragment do
|
|||||||
{{:unquoted_atom, acc}, offset} ->
|
{{:unquoted_atom, acc}, offset} ->
|
||||||
build_surround({:unquoted_atom, acc}, reversed, line, offset)
|
build_surround({:unquoted_atom, acc}, reversed, line, offset)
|
||||||
|
|
||||||
{{:capture_arg, acc}, offset} ->
|
|
||||||
build_surround({:capture_arg, acc}, reversed, line, offset)
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
maybe_operator(reversed_pre, post, line, opts)
|
maybe_operator(reversed_pre, post, line, opts)
|
||||||
end
|
end
|
||||||
@@ -851,16 +698,6 @@ defmodule Code.Fragment do
|
|||||||
reversed = reversed_post ++ reversed_pre
|
reversed = reversed_post ++ reversed_pre
|
||||||
|
|
||||||
case codepoint_cursor_context(reversed, opts) do
|
case codepoint_cursor_context(reversed, opts) do
|
||||||
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
|
|
||||||
arg = Enum.take_while(rest, &(&1 in ?0..?9))
|
|
||||||
|
|
||||||
build_surround(
|
|
||||||
{:capture_arg, ~c"&" ++ arg},
|
|
||||||
:lists.reverse(arg, reversed),
|
|
||||||
line,
|
|
||||||
offset + length(arg)
|
|
||||||
)
|
|
||||||
|
|
||||||
{{:operator, acc}, offset} ->
|
{{:operator, acc}, offset} ->
|
||||||
build_surround({:operator, acc}, reversed, line, offset)
|
build_surround({:operator, acc}, reversed, line, offset)
|
||||||
|
|
||||||
@@ -895,11 +732,27 @@ defmodule Code.Fragment do
|
|||||||
do: take_identifier(t, [h | acc])
|
do: take_identifier(t, [h | acc])
|
||||||
|
|
||||||
defp take_identifier(rest, acc) do
|
defp take_identifier(rest, acc) do
|
||||||
with {[?. | t], _} <- strip_spaces(rest, 0),
|
{stripped, _} = strip_spaces(rest, 0)
|
||||||
|
|
||||||
|
with [?. | t] <- stripped,
|
||||||
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
|
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
|
||||||
take_alias(rest, acc)
|
take_alias(rest, acc)
|
||||||
else
|
else
|
||||||
_ -> {:identifier, acc, rest}
|
# Consider it an identifier if we are at the end of line
|
||||||
|
# or if we have spaces not followed by . (call) or / (arity)
|
||||||
|
_ when acc == [] and (rest == [] or (hd(rest) in @space and hd(stripped) not in ~c"/.")) ->
|
||||||
|
{:identifier, acc, rest}
|
||||||
|
|
||||||
|
# If we are immediately followed by a container, we are still part of the identifier.
|
||||||
|
# We don't consider << as it _may_ be an operator.
|
||||||
|
_ when acc == [] and hd(stripped) in ~c"({[" ->
|
||||||
|
{:identifier, acc, rest}
|
||||||
|
|
||||||
|
_ when acc == [] ->
|
||||||
|
:none
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:identifier, acc, rest}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1207,23 +1060,16 @@ defmodule Code.Fragment do
|
|||||||
iex> Code.Fragment.container_cursor_to_quoted("foo +")
|
iex> Code.Fragment.container_cursor_to_quoted("foo +")
|
||||||
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
|
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
|
||||||
|
|
||||||
In order to parse the left-side of `->` properly, which appears both
|
|
||||||
in anonymous functions and do-end blocks, the trailing fragment option
|
|
||||||
must be given with the rest of the contents:
|
|
||||||
|
|
||||||
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
|
|
||||||
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
* `:file` - the filename to be reported in case of parsing errors.
|
* `:file` - the filename to be reported in case of parsing errors.
|
||||||
Defaults to `"nofile"`.
|
Defaults to `"nofile"`.
|
||||||
|
|
||||||
* `:line` - the starting line of the string being parsed.
|
* `:line` - the starting line of the string being parsed.
|
||||||
Defaults to `1`.
|
Defaults to 1.
|
||||||
|
|
||||||
* `:column` - the starting column of the string being parsed.
|
* `:column` - the starting column of the string being parsed.
|
||||||
Defaults to `1`.
|
Defaults to 1.
|
||||||
|
|
||||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||||
metadata. Defaults to `false`.
|
metadata. Defaults to `false`.
|
||||||
@@ -1236,151 +1082,14 @@ defmodule Code.Fragment do
|
|||||||
* `:literal_encoder` - a function to encode literals in the AST.
|
* `:literal_encoder` - a function to encode literals in the AST.
|
||||||
See the documentation for `Code.string_to_quoted/2` for more information.
|
See the documentation for `Code.string_to_quoted/2` for more information.
|
||||||
|
|
||||||
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
|
|
||||||
the cursor. This is necessary to correctly complete anonymous functions
|
|
||||||
and the left-hand side of `->`
|
|
||||||
|
|
||||||
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
|
|
||||||
(see "Tracking sigils" section below)
|
|
||||||
|
|
||||||
## Tracking sigils
|
|
||||||
|
|
||||||
The `:preserve_sigils` option can be used to track cursor positions inside
|
|
||||||
a sigil.
|
|
||||||
|
|
||||||
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
|
|
||||||
as the second argument:
|
|
||||||
|
|
||||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
|
|
||||||
{:ok,
|
|
||||||
{:sigil_r, [delimiter: "/", line: 1],
|
|
||||||
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
|
|
||||||
|
|
||||||
In case the sigil is completed and has zero or more modifiers, the cursor will
|
|
||||||
be nested in the list, with all previous delimiters specified:
|
|
||||||
|
|
||||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
|
|
||||||
{:ok,
|
|
||||||
{:sigil_r, [delimiter: "/", line: 1],
|
|
||||||
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
|
|
||||||
|
|
||||||
If the cursor is after the sigil, then it is discarded as everything else:
|
|
||||||
|
|
||||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
|
|
||||||
{:ok, {:__cursor__, [line: 1], []}}
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.13.0"
|
@doc since: "1.13.0"
|
||||||
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
|
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
|
||||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||||
def container_cursor_to_quoted(fragment, opts \\ []) do
|
def container_cursor_to_quoted(fragment, opts \\ []) do
|
||||||
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
|
opts =
|
||||||
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
|
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
|
||||||
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
|
|
||||||
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
|
|
||||||
|
|
||||||
file = Keyword.get(opts, :file, "nofile")
|
Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts)
|
||||||
line = Keyword.get(opts, :line, 1)
|
|
||||||
column = Keyword.get(opts, :column, 1)
|
|
||||||
|
|
||||||
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
|
|
||||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
|
|
||||||
when trailing_fragment == nil ->
|
|
||||||
{rev_tokens, rev_terminators} =
|
|
||||||
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
|
|
||||||
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
|
|
||||||
true <- maybe_missing_stab?(rev_tokens, false),
|
|
||||||
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
|
|
||||||
{[close, open, cursor | rev_tokens], after_fn}
|
|
||||||
else
|
|
||||||
_ -> {rev_tokens, rev_terminators}
|
|
||||||
end
|
|
||||||
|
|
||||||
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
|
|
||||||
|
|
||||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
|
||||||
{:ok, forms}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
|
|
||||||
tokens =
|
|
||||||
with {before_start, [_ | _] = after_start} <-
|
|
||||||
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
|
|
||||||
true <- maybe_missing_stab?(rev_tokens, true),
|
|
||||||
opts =
|
|
||||||
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
|
|
||||||
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
|
|
||||||
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
|
|
||||||
trailing_tokens =
|
|
||||||
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
|
|
||||||
|
|
||||||
# If the cursor has its own line, then we do not trim new lines trailing tokens.
|
|
||||||
# Otherwise we want to drop any newline so we drop the next tokens after eol.
|
|
||||||
trailing_tokens =
|
|
||||||
case rev_tokens do
|
|
||||||
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
|
|
||||||
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
|
|
||||||
end
|
|
||||||
|
|
||||||
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
|
|
||||||
else
|
|
||||||
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
|
|
||||||
end
|
|
||||||
|
|
||||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
|
||||||
{:ok, forms}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, info, _rest, _warnings, _so_far} ->
|
|
||||||
{:error, :elixir_tokenizer.format_error(info)}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp reverse_tokens(line, column, tokens, terminators) do
|
|
||||||
{terminators, _} =
|
|
||||||
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
|
|
||||||
atom = :elixir_tokenizer.terminator(start)
|
|
||||||
|
|
||||||
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Enum.reverse(tokens, terminators)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Otherwise we drop all tokens, trying to build a minimal AST
|
|
||||||
# for cursor completion.
|
|
||||||
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
|
|
||||||
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
|
|
||||||
|
|
||||||
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
|
||||||
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
|
||||||
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
|
||||||
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
|
||||||
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
|
||||||
|
|
||||||
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
|
||||||
|
|
||||||
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
|
|
||||||
defp drop_tokens([], _counter), do: []
|
|
||||||
|
|
||||||
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
|
|
||||||
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
|
|
||||||
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
|
|
||||||
defp maybe_missing_stab?([], _stab_choice?), do: false
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Code.Identifier do
|
defmodule Code.Identifier do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@@ -17,7 +13,7 @@ defmodule Code.Identifier do
|
|||||||
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
|
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
|
||||||
def unary_op(op) do
|
def unary_op(op) do
|
||||||
cond do
|
cond do
|
||||||
op in [:&, :...] -> {:non_associative, 90}
|
op in [:&] -> {:non_associative, 90}
|
||||||
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
|
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
|
||||||
op in [:@] -> {:non_associative, 320}
|
op in [:@] -> {:non_associative, 320}
|
||||||
true -> :error
|
true -> :error
|
||||||
@@ -48,6 +44,7 @@ defmodule Code.Identifier do
|
|||||||
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
|
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
|
||||||
op in [:in] -> {:left, 170}
|
op in [:in] -> {:left, 170}
|
||||||
op in [:"^^^"] -> {:left, 180}
|
op in [:"^^^"] -> {:left, 180}
|
||||||
|
op in [:"//"] -> {:right, 190}
|
||||||
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
|
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
|
||||||
op in [:+, :-] -> {:left, 210}
|
op in [:+, :-] -> {:left, 210}
|
||||||
op in [:*, :/] -> {:left, 220}
|
op in [:*, :/] -> {:left, 220}
|
||||||
@@ -65,7 +62,7 @@ defmodule Code.Identifier do
|
|||||||
with "-" <> rest <- Atom.to_string(atom),
|
with "-" <> rest <- Atom.to_string(atom),
|
||||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
|
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
end
|
end
|
||||||
@@ -75,11 +72,11 @@ defmodule Code.Identifier do
|
|||||||
Escapes the given identifier.
|
Escapes the given identifier.
|
||||||
"""
|
"""
|
||||||
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
|
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
|
||||||
{escaped :: binary(), remaining :: binary()}
|
{escaped :: iolist(), remaining :: binary()}
|
||||||
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
|
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
|
||||||
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
|
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
|
||||||
(is_integer(limit) and limit >= 0) do
|
(is_integer(limit) and limit >= 0) do
|
||||||
escape(binary, char, limit, <<>>, fun)
|
escape(binary, char, limit, [], fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
|
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
|
||||||
@@ -87,96 +84,64 @@ defmodule Code.Identifier do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<char, t::binary>>, char, count, acc, fun) do
|
defp escape(<<char, t::binary>>, char, count, acc, fun) do
|
||||||
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
|
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
|
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
|
||||||
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
|
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
|
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
|
||||||
if value = fun.(h) do
|
escaped = if value = fun.(h), do: value, else: escape_char(h)
|
||||||
value = IO.iodata_to_binary(value)
|
escape(t, char, decrement(count), [acc | escaped], fun)
|
||||||
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
|
|
||||||
else
|
|
||||||
escape(t, char, decrement(count), escape_char(h, acc), fun)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
|
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
|
||||||
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
|
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape(<<>>, _char, _count, acc, _fun) do
|
defp escape(<<>>, _char, _count, acc, _fun) do
|
||||||
{acc, <<>>}
|
{acc, <<>>}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
|
defp escape_char(0), do: [?\\, ?0]
|
||||||
|
|
||||||
defp escape_char(char, acc)
|
@escaped_bom :binary.bin_to_list("\\uFEFF")
|
||||||
# Some characters that are confusing (zero-width / alternative spaces) are displayed
|
defp escape_char(65279), do: @escaped_bom
|
||||||
# using their unicode representation:
|
|
||||||
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
|
|
||||||
|
|
||||||
# BOM
|
defp escape_char(char)
|
||||||
when char == 0xFEFF
|
|
||||||
# Mathematical invisibles
|
|
||||||
when char in 0x2061..0x2064
|
|
||||||
# Bidirectional neutral
|
|
||||||
when char in [0x061C, 0x200E, 0x200F]
|
|
||||||
# Bidirectional general (source of vulnerabilities)
|
|
||||||
when char in 0x202A..0x202E
|
|
||||||
when char in 0x2066..0x2069
|
|
||||||
# Interlinear annotations
|
|
||||||
when char in 0xFFF9..0xFFFC
|
|
||||||
# Zero-width joiners and non-joiners
|
|
||||||
when char in [0x200C, 0x200D, 0x034F]
|
|
||||||
# Non-break space / zero-width space
|
|
||||||
when char in [0x00A0, 0x200B, 0x2060]
|
|
||||||
# Line/paragraph separators
|
|
||||||
when char in [0x2028, 0x2029]
|
|
||||||
# Spaces
|
|
||||||
when char in 0x2000..0x200A
|
|
||||||
when char == 0x205F do
|
|
||||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
|
||||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
|
||||||
end
|
|
||||||
|
|
||||||
defp escape_char(char, acc)
|
|
||||||
when char in 0x20..0x7E
|
when char in 0x20..0x7E
|
||||||
when char in 0xA0..0xD7FF
|
when char in 0xA0..0xD7FF
|
||||||
when char in 0xE000..0xFFFD
|
when char in 0xE000..0xFFFD
|
||||||
when char in 0x10000..0x10FFFF do
|
when char in 0x10000..0x10FFFF do
|
||||||
<<acc::binary, char::utf8>>
|
<<char::utf8>>
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_char(char, acc) when char < 0x80 do
|
defp escape_char(char) when char < 0x100 do
|
||||||
<<a::4, b::4>> = <<char::8>>
|
<<a::4, b::4>> = <<char::8>>
|
||||||
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
|
[?\\, ?x, to_hex(a), to_hex(b)]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_char(char, acc) when char < 0x10000 do
|
defp escape_char(char) when char < 0x10000 do
|
||||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_char(char, acc) when char < 0x1000000 do
|
defp escape_char(char) when char < 0x1000000 do
|
||||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||||
|
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
|
||||||
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
|
||||||
?}>>
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp escape_map(?\a), do: "\\a"
|
defp escape_map(?\a), do: [?\\, ?a]
|
||||||
defp escape_map(?\b), do: "\\b"
|
defp escape_map(?\b), do: [?\\, ?b]
|
||||||
defp escape_map(?\d), do: "\\d"
|
defp escape_map(?\d), do: [?\\, ?d]
|
||||||
defp escape_map(?\e), do: "\\e"
|
defp escape_map(?\e), do: [?\\, ?e]
|
||||||
defp escape_map(?\f), do: "\\f"
|
defp escape_map(?\f), do: [?\\, ?f]
|
||||||
defp escape_map(?\n), do: "\\n"
|
defp escape_map(?\n), do: [?\\, ?n]
|
||||||
defp escape_map(?\r), do: "\\r"
|
defp escape_map(?\r), do: [?\\, ?r]
|
||||||
defp escape_map(?\t), do: "\\t"
|
defp escape_map(?\t), do: [?\\, ?t]
|
||||||
defp escape_map(?\v), do: "\\v"
|
defp escape_map(?\v), do: [?\\, ?v]
|
||||||
defp escape_map(?\\), do: "\\\\"
|
defp escape_map(?\\), do: [?\\, ?\\]
|
||||||
defp escape_map(_), do: false
|
defp escape_map(_), do: false
|
||||||
|
|
||||||
@compile {:inline, to_hex: 1, decrement: 1}
|
@compile {:inline, to_hex: 1, decrement: 1}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
|
|
||||||
defmodule Code.Normalizer do
|
defmodule Code.Normalizer do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@do_end_keywords [:rescue, :catch, :else, :after]
|
|
||||||
|
|
||||||
defguard is_literal(x)
|
defguard is_literal(x)
|
||||||
when is_integer(x) or
|
when is_integer(x) or
|
||||||
is_float(x) or
|
is_float(x) or
|
||||||
@@ -16,7 +11,6 @@ defmodule Code.Normalizer do
|
|||||||
Wraps literals in the quoted expression to conform to the AST format expected
|
Wraps literals in the quoted expression to conform to the AST format expected
|
||||||
by the formatter.
|
by the formatter.
|
||||||
"""
|
"""
|
||||||
@spec normalize(Macro.t(), keyword()) :: Macro.t()
|
|
||||||
def normalize(quoted, opts \\ []) do
|
def normalize(quoted, opts \\ []) do
|
||||||
line = Keyword.get(opts, :line, nil)
|
line = Keyword.get(opts, :line, nil)
|
||||||
escape = Keyword.get(opts, :escape, true)
|
escape = Keyword.get(opts, :escape, true)
|
||||||
@@ -64,13 +58,13 @@ defmodule Code.Normalizer do
|
|||||||
{:.., meta, [left, right]}
|
{:.., meta, [left, right]}
|
||||||
else
|
else
|
||||||
step = do_normalize(step, state)
|
step = do_normalize(step, state)
|
||||||
{:..//, meta, [left, right, step]}
|
{:"..//", meta, [left, right, step]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Bit containers
|
# Bit containers
|
||||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||||
normalize_bitstring(quoted, state, state.escape)
|
normalize_bitstring(quoted, state)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Atoms with interpolations
|
# Atoms with interpolations
|
||||||
@@ -91,12 +85,17 @@ defmodule Code.Normalizer do
|
|||||||
normalize_literal(:utf8, [], state)
|
normalize_literal(:utf8, [], state)
|
||||||
end
|
end
|
||||||
|
|
||||||
string = normalize_bitstring(string, state, state.escape)
|
string =
|
||||||
|
if state.escape do
|
||||||
|
normalize_bitstring(string, state, true)
|
||||||
|
else
|
||||||
|
normalize_bitstring(string, state)
|
||||||
|
end
|
||||||
|
|
||||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Charlists with interpolations
|
# Charlists with interpolations
|
||||||
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
|
|
||||||
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
|
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
|
||||||
if list_interpolated?(parts) do
|
if list_interpolated?(parts) do
|
||||||
parts =
|
parts =
|
||||||
@@ -114,7 +113,6 @@ defmodule Code.Normalizer do
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
parts = maybe_add_trailing_newline(call_meta, parts, state)
|
|
||||||
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
|
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
|
||||||
else
|
else
|
||||||
normalize_call(quoted, state)
|
normalize_call(quoted, state)
|
||||||
@@ -127,17 +125,22 @@ defmodule Code.Normalizer do
|
|||||||
{:., meta, [Access, :get]}
|
{:., meta, [Access, :get]}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only normalize the left side of the dot operator
|
||||||
# The right hand side is an atom in the AST but it's not an atom literal, so
|
# The right hand side is an atom in the AST but it's not an atom literal, so
|
||||||
# it should not be wrapped. However, it should be escaped if applicable.
|
# it should not be wrapped
|
||||||
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
|
defp do_normalize({:., meta, [left, right]}, state) do
|
||||||
meta = patch_meta_line(meta, state.parent_meta)
|
meta = patch_meta_line(meta, state.parent_meta)
|
||||||
|
|
||||||
left = do_normalize(left, %{state | parent_meta: meta})
|
left = do_normalize(left, %{state | parent_meta: meta})
|
||||||
right = maybe_escape_literal(right, state)
|
|
||||||
|
|
||||||
{:., meta, [left, right]}
|
{:., meta, [left, right]}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
|
||||||
|
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
|
||||||
|
normalize_args(quoted, state)
|
||||||
|
end
|
||||||
|
|
||||||
# left -> right
|
# left -> right
|
||||||
defp do_normalize({:->, meta, [left, right]}, state) do
|
defp do_normalize({:->, meta, [left, right]}, state) do
|
||||||
meta = patch_meta_line(meta, state.parent_meta)
|
meta = patch_meta_line(meta, state.parent_meta)
|
||||||
@@ -166,6 +169,9 @@ defmodule Code.Normalizer do
|
|||||||
right = normalize_map_args(right, state)
|
right = normalize_map_args(right, state)
|
||||||
[{:|, pipe_meta, [left, right]}]
|
[{:|, pipe_meta, [left, right]}]
|
||||||
|
|
||||||
|
[{_, _, _} = call] ->
|
||||||
|
[do_normalize(call, state)]
|
||||||
|
|
||||||
args ->
|
args ->
|
||||||
normalize_map_args(args, state)
|
normalize_map_args(args, state)
|
||||||
end
|
end
|
||||||
@@ -175,18 +181,16 @@ defmodule Code.Normalizer do
|
|||||||
|
|
||||||
# Sigils
|
# Sigils
|
||||||
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
|
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
|
||||||
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
|
when is_list(args) and is_atom(sigil) do
|
||||||
with "sigil_" <> _ <- Atom.to_string(sigil),
|
case Atom.to_string(sigil) do
|
||||||
true <- binary_interpolated?(args),
|
"sigil_" <> _ ->
|
||||||
true <- List.ascii_printable?(modifiers) do
|
meta =
|
||||||
meta =
|
meta
|
||||||
meta
|
|> patch_meta_line(state.parent_meta)
|
||||||
|> patch_meta_line(state.parent_meta)
|
|> Keyword.put_new(:delimiter, "\"")
|
||||||
|> Keyword.put_new(:delimiter, "\"")
|
|
||||||
|
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||||
|
|
||||||
string = normalize_bitstring(string, %{state | parent_meta: meta}, false)
|
|
||||||
{sigil, meta, [string, modifiers]}
|
|
||||||
else
|
|
||||||
_ ->
|
_ ->
|
||||||
normalize_call(quoted, state)
|
normalize_call(quoted, state)
|
||||||
end
|
end
|
||||||
@@ -267,7 +271,7 @@ defmodule Code.Normalizer do
|
|||||||
"Elixir." <> segments ->
|
"Elixir." <> segments ->
|
||||||
segments
|
segments
|
||||||
|> String.split(".")
|
|> String.split(".")
|
||||||
|> Enum.map(&String.to_unsafe_atom/1)
|
|> Enum.map(&String.to_atom/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:__aliases__, meta, segments}
|
{:__aliases__, meta, segments}
|
||||||
@@ -291,18 +295,21 @@ defmodule Code.Normalizer do
|
|||||||
# Lists
|
# Lists
|
||||||
defp normalize_literal(list, meta, state) when is_list(list) do
|
defp normalize_literal(list, meta, state) when is_list(list) do
|
||||||
if list != [] and List.ascii_printable?(list) do
|
if list != [] and List.ascii_printable?(list) do
|
||||||
# It's a charlist, we normalize it as a ~C sigil
|
# It's a charlist
|
||||||
string =
|
list =
|
||||||
if state.escape do
|
if state.escape do
|
||||||
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
|
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
|
||||||
IO.iodata_to_binary(iolist)
|
IO.iodata_to_binary(string) |> to_charlist()
|
||||||
else
|
else
|
||||||
List.to_string(list)
|
list
|
||||||
end
|
end
|
||||||
|
|
||||||
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
|
meta =
|
||||||
|
meta
|
||||||
|
|> Keyword.put_new(:delimiter, "'")
|
||||||
|
|> patch_meta_line(state.parent_meta)
|
||||||
|
|
||||||
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
|
{:__block__, meta, [list]}
|
||||||
else
|
else
|
||||||
meta =
|
meta =
|
||||||
if line = state.parent_meta[:line] do
|
if line = state.parent_meta[:line] do
|
||||||
@@ -342,46 +349,29 @@ defmodule Code.Normalizer do
|
|||||||
meta
|
meta
|
||||||
end
|
end
|
||||||
|
|
||||||
last = List.last(args)
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
not allow_keyword?(form, arity) ->
|
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) ->
|
||||||
args = normalize_args(args, %{state | parent_meta: meta})
|
|
||||||
{form, meta, args}
|
|
||||||
|
|
||||||
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
|
|
||||||
# def foo do :ok end
|
# def foo do :ok end
|
||||||
# def foo, do: :ok
|
# def foo, do: :ok
|
||||||
normalize_kw_blocks(form, meta, args, state)
|
normalize_kw_blocks(form, meta, args, state)
|
||||||
|
|
||||||
match?([{:do, _} | _], last) and kw_blocks?(last) ->
|
match?([{:do, _} | _], List.last(args)) ->
|
||||||
# Non normalized kw blocks
|
# Non normalized kw blocks
|
||||||
line = state.parent_meta[:line] || meta[:line]
|
line = state.parent_meta[:line]
|
||||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||||
normalize_kw_blocks(form, meta, args, state)
|
normalize_kw_blocks(form, meta, args, state)
|
||||||
|
|
||||||
true ->
|
allow_keyword?(form, arity) ->
|
||||||
# The formatter renders do-end blocks from the meta alone
|
|
||||||
meta = Keyword.drop(meta, [:do, :end])
|
|
||||||
args = normalize_args(args, %{state | parent_meta: meta})
|
args = normalize_args(args, %{state | parent_meta: meta})
|
||||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||||
|
|
||||||
last_args =
|
last_args =
|
||||||
case last_arg do
|
case last_arg do
|
||||||
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
|
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
|
||||||
cond do
|
if key_meta[:format] == :keyword do
|
||||||
key_meta[:format] == :keyword ->
|
last_args
|
||||||
[keyword]
|
else
|
||||||
|
[last_arg]
|
||||||
block_keyword?(keyword) ->
|
|
||||||
[
|
|
||||||
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
|
|
||||||
{{:__block__, [format: :keyword] ++ meta, args}, value}
|
|
||||||
end)
|
|
||||||
]
|
|
||||||
|
|
||||||
true ->
|
|
||||||
[last_arg]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
[] ->
|
[] ->
|
||||||
@@ -392,31 +382,18 @@ defmodule Code.Normalizer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
{form, meta, leading_args ++ last_args}
|
{form, meta, leading_args ++ last_args}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
args = normalize_args(args, %{state | parent_meta: meta})
|
||||||
|
{form, meta, args}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
|
|
||||||
do: block_keyword?(tail)
|
|
||||||
|
|
||||||
defp block_keyword?([]), do: true
|
|
||||||
defp block_keyword?(_), do: false
|
|
||||||
|
|
||||||
# Anything after the do block that is not a block keyword makes it a keyword list
|
|
||||||
defp kw_blocks?([{:do, _} | rest] = kw) do
|
|
||||||
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
|
|
||||||
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp kw_blocks?(_), do: false
|
|
||||||
|
|
||||||
defp allow_keyword?(:when, 2), do: true
|
defp allow_keyword?(:when, 2), do: true
|
||||||
defp allow_keyword?(:{}, _), do: false
|
defp allow_keyword?(:{}, _), do: false
|
||||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||||
|
|
||||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
|
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||||
meta = patch_meta_line(meta, state.parent_meta)
|
meta = patch_meta_line(meta, state.parent_meta)
|
||||||
|
|
||||||
parts =
|
parts =
|
||||||
@@ -435,21 +412,9 @@ defmodule Code.Normalizer do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
|
||||||
{:<<>>, meta, parts}
|
{:<<>>, meta, parts}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_trailing_newline(meta, parts, state) do
|
|
||||||
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
|
|
||||||
last = List.last(parts),
|
|
||||||
true <- is_binary(last) and not String.ends_with?(last, "\n") do
|
|
||||||
[_last | rest] = Enum.reverse(parts)
|
|
||||||
Enum.reverse([last <> "\n" | rest])
|
|
||||||
else
|
|
||||||
_ -> parts
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
|
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
|
||||||
Enum.map(parts, fn
|
Enum.map(parts, fn
|
||||||
{:"::", interpolation_meta,
|
{:"::", interpolation_meta,
|
||||||
@@ -575,7 +540,7 @@ defmodule Code.Normalizer do
|
|||||||
atom
|
atom
|
||||||
|> Atom.to_string()
|
|> Atom.to_string()
|
||||||
|> maybe_escape_literal(state)
|
|> maybe_escape_literal(state)
|
||||||
|> String.to_unsafe_atom()
|
|> String.to_atom()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_escape_literal(term, _) do
|
defp maybe_escape_literal(term, _) do
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Code.Typespec do
|
defmodule Code.Typespec do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@@ -31,6 +27,11 @@ defmodule Code.Typespec do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
|
||||||
|
meta = meta(anno)
|
||||||
|
{:"::", meta, [{name, meta, []}, quote(do: term)]}
|
||||||
|
end
|
||||||
|
|
||||||
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
|
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
|
||||||
meta = meta(anno)
|
meta = meta(anno)
|
||||||
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
|
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
|
||||||
@@ -80,8 +81,7 @@ defmodule Code.Typespec do
|
|||||||
Returns all types available from the module's BEAM code.
|
Returns all types available from the module's BEAM code.
|
||||||
|
|
||||||
The result is returned as a list of tuples where the first
|
The result is returned as a list of tuples where the first
|
||||||
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+,
|
element is the type (`:typep`, `:type` and `:opaque`).
|
||||||
`:nominal`).
|
|
||||||
|
|
||||||
The module must have a corresponding BEAM file which can be
|
The module must have a corresponding BEAM file which can be
|
||||||
located by the runtime system. The types will be in the Erlang
|
located by the runtime system. The types will be in the Erlang
|
||||||
@@ -96,10 +96,9 @@ defmodule Code.Typespec do
|
|||||||
|
|
||||||
types =
|
types =
|
||||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
||||||
kind in [:opaque, :type, :nominal] do
|
kind in [:opaque, :type] do
|
||||||
cond do
|
cond do
|
||||||
kind == :opaque -> {:opaque, type}
|
kind == :opaque -> {:opaque, type}
|
||||||
kind == :nominal -> {:nominal, type}
|
|
||||||
{name, length(args)} in exported_types -> {:type, type}
|
{name, length(args)} in exported_types -> {:type, type}
|
||||||
true -> {:typep, type}
|
true -> {:typep, type}
|
||||||
end
|
end
|
||||||
@@ -119,10 +118,10 @@ defmodule Code.Typespec do
|
|||||||
element is spec name and arity and the second is the spec.
|
element is spec name and arity and the second is the spec.
|
||||||
|
|
||||||
The module must have a corresponding BEAM file which can be
|
The module must have a corresponding BEAM file which can be
|
||||||
located by the runtime system. The specs will be in the Erlang
|
located by the runtime system. The types will be in the Erlang
|
||||||
Abstract Format.
|
Abstract Format.
|
||||||
"""
|
"""
|
||||||
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
|
@spec fetch_specs(module) :: {:ok, [tuple]} | :error
|
||||||
def fetch_specs(module) when is_atom(module) or is_binary(module) do
|
def fetch_specs(module) when is_atom(module) or is_binary(module) do
|
||||||
case typespecs_abstract_code(module) do
|
case typespecs_abstract_code(module) do
|
||||||
{:ok, abstract_code} ->
|
{:ok, abstract_code} ->
|
||||||
@@ -137,13 +136,13 @@ defmodule Code.Typespec do
|
|||||||
Returns all callbacks available from the module's BEAM code.
|
Returns all callbacks available from the module's BEAM code.
|
||||||
|
|
||||||
The result is returned as a list of tuples where the first
|
The result is returned as a list of tuples where the first
|
||||||
element is the callback name and arity and the second is the callback.
|
element is spec name and arity and the second is the spec.
|
||||||
|
|
||||||
The module must have a corresponding BEAM file
|
The module must have a corresponding BEAM file
|
||||||
which can be located by the runtime system. The callbacks will be
|
which can be located by the runtime system. The types will be
|
||||||
in the Erlang Abstract Format.
|
in the Erlang Abstract Format.
|
||||||
"""
|
"""
|
||||||
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
|
@spec fetch_callbacks(module) :: {:ok, [tuple]} | :error
|
||||||
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
|
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
|
||||||
case typespecs_abstract_code(module) do
|
case typespecs_abstract_code(module) do
|
||||||
{:ok, abstract_code} ->
|
{:ok, abstract_code} ->
|
||||||
@@ -176,8 +175,7 @@ defmodule Code.Typespec do
|
|||||||
|
|
||||||
defp get_module_and_beam(module) when is_atom(module) do
|
defp get_module_and_beam(module) when is_atom(module) do
|
||||||
with {^module, beam, _filename} <- :code.get_object_code(module),
|
with {^module, beam, _filename} <- :code.get_object_code(module),
|
||||||
info_pairs when is_list(info_pairs) <- :beam_lib.info(beam),
|
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
|
||||||
{:ok, ^module} <- Keyword.fetch(info_pairs, :module) do
|
|
||||||
{module, beam}
|
{module, beam}
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
@@ -193,8 +191,8 @@ defmodule Code.Typespec do
|
|||||||
|
|
||||||
## To AST conversion
|
## To AST conversion
|
||||||
|
|
||||||
defp collect_vars({:ann_type, _anno, [_var, type]}) do
|
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
|
||||||
collect_vars(type)
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
||||||
@@ -289,6 +287,7 @@ defmodule Code.Typespec do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
|
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
|
||||||
|
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
|
||||||
line = meta(anno)[:line]
|
line = meta(anno)[:line]
|
||||||
|
|
||||||
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
|
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
|
||||||
@@ -317,6 +316,10 @@ defmodule Code.Typespec do
|
|||||||
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
|
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp typespec_to_quoted({:type, anno, :fun, []}) do
|
||||||
|
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
|
||||||
|
end
|
||||||
|
|
||||||
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
|
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
|
||||||
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
|
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
|
||||||
end
|
end
|
||||||
@@ -334,14 +337,10 @@ defmodule Code.Typespec do
|
|||||||
{erl_to_ex_var(var), meta(anno), nil}
|
{erl_to_ex_var(var), meta(anno), nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
|
defp typespec_to_quoted({:op, anno, op, arg}) do
|
||||||
{op, meta(anno), [typespec_to_quoted(arg)]}
|
{op, meta(anno), [typespec_to_quoted(arg)]}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
|
|
||||||
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
|
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
|
||||||
remote_type(anno, mod, name, args)
|
remote_type(anno, mod, name, args)
|
||||||
end
|
end
|
||||||
@@ -401,10 +400,10 @@ defmodule Code.Typespec do
|
|||||||
defp erl_to_ex_var(var) do
|
defp erl_to_ex_var(var) do
|
||||||
case Atom.to_string(var) do
|
case Atom.to_string(var) do
|
||||||
<<"_", c::utf8, rest::binary>> ->
|
<<"_", c::utf8, rest::binary>> ->
|
||||||
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||||
|
|
||||||
<<c::utf8, rest::binary>> ->
|
<<c::utf8, rest::binary>> ->
|
||||||
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -420,13 +419,5 @@ defmodule Code.Typespec do
|
|||||||
:error
|
:error
|
||||||
end
|
end
|
||||||
|
|
||||||
defp meta(anno) do
|
defp meta(anno), do: [line: :erl_anno.line(anno)]
|
||||||
case :erl_anno.location(anno) do
|
|
||||||
{line, column} ->
|
|
||||||
[line: line, column: column]
|
|
||||||
|
|
||||||
line when is_integer(line) ->
|
|
||||||
[line: line]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defprotocol Collectable do
|
defprotocol Collectable do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A protocol to traverse data structures.
|
A protocol to traverse data structures.
|
||||||
@@ -69,22 +65,6 @@ defprotocol Collectable do
|
|||||||
iex> Enum.into([1, 2, 3], MapSet.new())
|
iex> Enum.into([1, 2, 3], MapSet.new())
|
||||||
MapSet.new([1, 2, 3])
|
MapSet.new([1, 2, 3])
|
||||||
|
|
||||||
## Halting
|
|
||||||
|
|
||||||
The `:halt` flag will be given whenever the collection won't
|
|
||||||
terminate correctly and must be used to clean up existing resources
|
|
||||||
(such as sockets, file handles, etc).
|
|
||||||
|
|
||||||
Note it is not guaranteed that the accumulator given to halt will
|
|
||||||
be the latest version of the accumulator returned by a previous call
|
|
||||||
with `{:cont, elem}`. Therefore, you must track the collected results
|
|
||||||
within the resource you intend to halt.
|
|
||||||
|
|
||||||
This is by design: ensuring halt is always called with the latest
|
|
||||||
accumulator would make pure collectables (the ones that do not implement
|
|
||||||
halt) expensive. However, given the collectables that must implement halt
|
|
||||||
already need to track state, the burden of tracking the accumulator
|
|
||||||
across invocations is put on them.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type command :: {:cont, term} | :done | :halt
|
@type command :: {:cont, term} | :done | :halt
|
||||||
@@ -114,10 +94,10 @@ end
|
|||||||
|
|
||||||
defimpl Collectable, for: List do
|
defimpl Collectable, for: List do
|
||||||
def into(list) do
|
def into(list) do
|
||||||
# TODO: Change the behavior so the into always comes last on Elixir v2.0
|
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
|
||||||
if list != [] do
|
if list != [] do
|
||||||
IO.warn(
|
IO.warn(
|
||||||
"the Collectable protocol is deprecated for non-empty lists. The behavior of " <>
|
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
|
||||||
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
|
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
|
||||||
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
|
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
|
||||||
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
|
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
|
||||||
@@ -160,10 +140,6 @@ defimpl Collectable, for: BitString do
|
|||||||
|
|
||||||
__acc, :halt ->
|
__acc, :halt ->
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_acc, {:cont, other} ->
|
|
||||||
raise ArgumentError,
|
|
||||||
"collecting into a binary requires a bitstring, got: #{inspect(other)}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{[binary], fun}
|
{[binary], fun}
|
||||||
@@ -179,10 +155,6 @@ defimpl Collectable, for: BitString do
|
|||||||
|
|
||||||
_acc, :halt ->
|
_acc, :halt ->
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_acc, {:cont, other} ->
|
|
||||||
raise ArgumentError,
|
|
||||||
"collecting into a bitstring requires a bitstring, got: #{inspect(other)}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{bitstring, fun}
|
{bitstring, fun}
|
||||||
@@ -200,10 +172,6 @@ defimpl Collectable, for: Map do
|
|||||||
|
|
||||||
_map_acc, :halt ->
|
_map_acc, :halt ->
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_map_acc, {:cont, other} ->
|
|
||||||
raise ArgumentError,
|
|
||||||
"collecting into a map requires {key, value} tuples, got: #{inspect(other)}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{map, fun}
|
{map, fun}
|
||||||
|
|||||||
+21
-66
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Config do
|
defmodule Config do
|
||||||
@moduledoc ~S"""
|
@moduledoc ~S"""
|
||||||
A simple keyword-based configuration API.
|
A simple keyword-based configuration API.
|
||||||
@@ -45,15 +41,14 @@ defmodule Config do
|
|||||||
> application environment is effectively a global storage. Also note that
|
> application environment is effectively a global storage. Also note that
|
||||||
> the `config/config.exs` of a library is not evaluated when the library is
|
> the `config/config.exs` of a library is not evaluated when the library is
|
||||||
> used as a dependency, as configuration is always meant to configure the
|
> used as a dependency, as configuration is always meant to configure the
|
||||||
> current project. For more information, see ["Using application configuration for
|
> current project. For more information, read our [library guidelines](library-guidelines.md).
|
||||||
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
|
|
||||||
|
|
||||||
## Migrating from `use Mix.Config`
|
## Migrating from `use Mix.Config`
|
||||||
|
|
||||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||||
`use Mix.Config`, which was specific to Mix and has been deprecated.
|
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||||
|
|
||||||
You can leverage `Config` instead of `use Mix.Config` in three steps. The first
|
You can leverage `Config` instead of `Mix.Config` in three steps. The first
|
||||||
step is to replace `use Mix.Config` at the top of your config files by
|
step is to replace `use Mix.Config` at the top of your config files by
|
||||||
`import Config`.
|
`import Config`.
|
||||||
|
|
||||||
@@ -87,23 +82,17 @@ defmodule Config do
|
|||||||
...
|
...
|
||||||
end
|
end
|
||||||
|
|
||||||
The only places where you may access functions from the `Mix` module are
|
The only files where you may access functions from the `Mix` module are
|
||||||
the `mix.exs` file and inside custom Mix tasks, which are always within
|
the `mix.exs` file and inside custom Mix tasks, which always within the
|
||||||
the `Mix.Tasks` namespace.
|
`Mix.Tasks` namespace.
|
||||||
|
|
||||||
## `config/runtime.exs`
|
## config/runtime.exs
|
||||||
|
|
||||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||||
It is executed right before applications start in both Mix and releases
|
It is executed right before applications start in both Mix and releases
|
||||||
(assembled with `mix release`).
|
(assembled with `mix release`).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type config_opts :: [
|
|
||||||
imports: [Path.t()] | :disabled,
|
|
||||||
env: atom(),
|
|
||||||
target: atom()
|
|
||||||
]
|
|
||||||
|
|
||||||
@opts_key {__MODULE__, :opts}
|
@opts_key {__MODULE__, :opts}
|
||||||
@config_key {__MODULE__, :config}
|
@config_key {__MODULE__, :config}
|
||||||
@imports_key {__MODULE__, :imports}
|
@imports_key {__MODULE__, :imports}
|
||||||
@@ -141,6 +130,7 @@ defmodule Config do
|
|||||||
|
|
||||||
config :logger,
|
config :logger,
|
||||||
level: :warn,
|
level: :warn,
|
||||||
|
backends: [:console]
|
||||||
|
|
||||||
config :logger,
|
config :logger,
|
||||||
level: :info,
|
level: :info,
|
||||||
@@ -148,13 +138,12 @@ defmodule Config do
|
|||||||
|
|
||||||
will have a final configuration for `:logger` of:
|
will have a final configuration for `:logger` of:
|
||||||
|
|
||||||
[level: :info, truncate: 1024]
|
[level: :info, backends: [:console], truncate: 1024]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.9.0"
|
@doc since: "1.9.0"
|
||||||
@spec config(atom(), keyword()) :: keyword()
|
|
||||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||||
if not Keyword.keyword?(opts) do
|
unless Keyword.keyword?(opts) do
|
||||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -199,45 +188,17 @@ defmodule Config do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.9.0"
|
@doc since: "1.9.0"
|
||||||
@spec config(atom(), atom(), term()) :: keyword()
|
|
||||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||||
get_config!()
|
get_config!()
|
||||||
|> __merge__([{root_key, [{key, opts}]}])
|
|> __merge__([{root_key, [{key, opts}]}])
|
||||||
|> put_config()
|
|> put_config()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Reads the configuration for the given root key.
|
|
||||||
|
|
||||||
This function only reads the configuration from a previous
|
|
||||||
`config/2` or `config/3` call. If `root_key` points to an
|
|
||||||
application, it does not read its actual application environment.
|
|
||||||
Its main use case is to make it easier to access and share
|
|
||||||
configuration values across files.
|
|
||||||
|
|
||||||
If the `root_key` was not configured, it returns `nil`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
# In config/config.exs
|
|
||||||
config :my_app, foo: :bar
|
|
||||||
|
|
||||||
# In config/dev.exs
|
|
||||||
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
|
|
||||||
|
|
||||||
"""
|
|
||||||
@doc since: "1.18.0"
|
|
||||||
@spec read_config(atom()) :: keyword() | nil
|
|
||||||
def read_config(root_key) when is_atom(root_key) do
|
|
||||||
get_config!()[root_key]
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the environment this configuration file is executed on.
|
Returns the environment this configuration file is executed on.
|
||||||
|
|
||||||
In Mix projects this function returns the environment this configuration
|
In Mix projects this function returns the environment this configuration
|
||||||
file is executed on.
|
file is executed on. In releases, the environment when `mix release` ran.
|
||||||
In releases, returns the `MIX_ENV` specified when running `mix release`.
|
|
||||||
|
|
||||||
This is most often used to execute conditional code:
|
This is most often used to execute conditional code:
|
||||||
|
|
||||||
@@ -287,8 +248,8 @@ defmodule Config do
|
|||||||
|
|
||||||
In case the file doesn't exist, an error is raised.
|
In case the file doesn't exist, an error is raised.
|
||||||
|
|
||||||
If the file is relative, it will be expanded relative to the
|
If file is a relative, it will be expanded relatively to the
|
||||||
directory of the current configuration file.
|
directory the current configuration file is in.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -315,7 +276,7 @@ defmodule Config do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
|
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
|
||||||
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||||
env = Keyword.get(opts, :env)
|
env = Keyword.get(opts, :env)
|
||||||
target = Keyword.get(opts, :target)
|
target = Keyword.get(opts, :target)
|
||||||
@@ -380,27 +341,21 @@ defmodule Config do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate!(config, file) when is_list(config) do
|
defp validate!(config, file) do
|
||||||
Enum.each(config, fn
|
Enum.all?(config, fn
|
||||||
{app, value} when is_atom(app) ->
|
{app, value} when is_atom(app) ->
|
||||||
if not Keyword.keyword?(value) do
|
if Keyword.keyword?(value) do
|
||||||
|
true
|
||||||
|
else
|
||||||
raise ArgumentError,
|
raise ArgumentError,
|
||||||
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
||||||
"to return keyword list, got: #{inspect(value)}"
|
"to return keyword list, got: #{inspect(value)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
other ->
|
_ ->
|
||||||
raise ArgumentError,
|
false
|
||||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
|
||||||
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
config
|
config
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate!(config, file) do
|
|
||||||
raise ArgumentError,
|
|
||||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
|
||||||
"of {atom, keyword} pairs, got: #{inspect(config)}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Config.Provider do
|
defmodule Config.Provider do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Specifies a provider API that loads configuration during boot.
|
Specifies a provider API that loads configuration during boot.
|
||||||
@@ -25,7 +21,7 @@ defmodule Config.Provider do
|
|||||||
For example, imagine you want to list some basic configuration
|
For example, imagine you want to list some basic configuration
|
||||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||||
to support additional configuration files. To do so, you can add
|
to support additional configuration files. To do so, you can add
|
||||||
this inside the `def project` portion of your `mix.exs`:
|
this inside the `def project` portion of your `mix.exs`:
|
||||||
|
|
||||||
releases: [
|
releases: [
|
||||||
demo: [
|
demo: [
|
||||||
@@ -111,16 +107,6 @@ defmodule Config.Provider do
|
|||||||
"""
|
"""
|
||||||
@type config_path :: {:system, binary(), binary()} | binary()
|
@type config_path :: {:system, binary(), binary()} | binary()
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Options for `init/3`.
|
|
||||||
"""
|
|
||||||
@type init_opts :: [
|
|
||||||
extra_config: config(),
|
|
||||||
prune_runtime_sys_config_after_boot: boolean(),
|
|
||||||
reboot_system_after_config: boolean(),
|
|
||||||
validate_compile_env: [{atom(), [atom()], term()}]
|
|
||||||
]
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Invoked when initializing a config provider.
|
Invoked when initializing a config provider.
|
||||||
|
|
||||||
@@ -206,7 +192,6 @@ defmodule Config.Provider do
|
|||||||
@reboot_mode_key :config_provider_reboot_mode
|
@reboot_mode_key :config_provider_reboot_mode
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
|
|
||||||
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
||||||
validate_config_path!(config_path)
|
validate_config_path!(config_path)
|
||||||
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
||||||
@@ -327,7 +312,7 @@ defmodule Config.Provider do
|
|||||||
"""
|
"""
|
||||||
the application #{inspect(app)} has a different value set #{path(key, path)} \
|
the application #{inspect(app)} has a different value set #{path(key, path)} \
|
||||||
during runtime compared to compile time. Since this application environment entry was \
|
during runtime compared to compile time. Since this application environment entry was \
|
||||||
marked as compile time, this difference can lead to different behavior than expected:
|
marked as compile time, this difference can lead to different behaviour than expected:
|
||||||
|
|
||||||
* Compile time value #{return_to_text(compile_return)}
|
* Compile time value #{return_to_text(compile_return)}
|
||||||
* Runtime value #{return_to_text(runtime_return)}
|
* Runtime value #{return_to_text(runtime_return)}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Config.Reader do
|
defmodule Config.Reader do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
API for reading config files defined with `Config`.
|
API for reading config files defined with `Config`.
|
||||||
@@ -16,7 +12,7 @@ defmodule Config.Reader do
|
|||||||
|
|
||||||
For example, if you expect the target system to have a config file
|
For example, if you expect the target system to have a config file
|
||||||
in an absolute path, you can add this inside the `def project` portion
|
in an absolute path, you can add this inside the `def project` portion
|
||||||
of your `mix.exs`:
|
of your `mix.exs`:
|
||||||
|
|
||||||
releases: [
|
releases: [
|
||||||
demo: [
|
demo: [
|
||||||
@@ -46,12 +42,6 @@ defmodule Config.Reader do
|
|||||||
|
|
||||||
@behaviour Config.Provider
|
@behaviour Config.Provider
|
||||||
|
|
||||||
@type config_opts :: [
|
|
||||||
imports: [Path.t()] | :disabled,
|
|
||||||
env: atom(),
|
|
||||||
target: atom()
|
|
||||||
]
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(opts) when is_list(opts) do
|
def init(opts) when is_list(opts) do
|
||||||
{path, opts} = Keyword.pop!(opts, :path)
|
{path, opts} = Keyword.pop!(opts, :path)
|
||||||
@@ -74,7 +64,7 @@ defmodule Config.Reader do
|
|||||||
Accepts the same options as `read!/2`.
|
Accepts the same options as `read!/2`.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.11.0"
|
@doc since: "1.11.0"
|
||||||
@spec eval!(Path.t(), binary, config_opts) :: keyword
|
@spec eval!(Path.t(), binary, keyword) :: keyword
|
||||||
def eval!(file, contents, opts \\ [])
|
def eval!(file, contents, opts \\ [])
|
||||||
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
||||||
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
||||||
@@ -96,7 +86,7 @@ defmodule Config.Reader do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.9.0"
|
@doc since: "1.9.0"
|
||||||
@spec read!(Path.t(), config_opts) :: keyword
|
@spec read!(Path.t(), keyword) :: keyword
|
||||||
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||||
file = Path.expand(file)
|
file = Path.expand(file)
|
||||||
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
||||||
@@ -110,7 +100,7 @@ defmodule Config.Reader do
|
|||||||
option cannot be disabled in `read_imports!/2`.
|
option cannot be disabled in `read_imports!/2`.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.9.0"
|
@doc since: "1.9.0"
|
||||||
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
|
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
|
||||||
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||||
if opts[:imports] == :disabled do
|
if opts[:imports] == :disabled do
|
||||||
raise ArgumentError, ":imports must be a list of paths"
|
raise ArgumentError, ":imports must be a list of paths"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Dict do
|
defmodule Dict do
|
||||||
@moduledoc ~S"""
|
@moduledoc ~S"""
|
||||||
Generic API for dictionaries.
|
Generic API for dictionaries.
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule DynamicSupervisor do
|
defmodule DynamicSupervisor do
|
||||||
@moduledoc ~S"""
|
@moduledoc ~S"""
|
||||||
A supervisor optimized to only start children dynamically.
|
A supervisor optimized to only start children dynamically.
|
||||||
@@ -16,7 +12,7 @@ defmodule DynamicSupervisor do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
A dynamic supervisor is started with no children and often with a name:
|
A dynamic supervisor is started with no children and often a name:
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
|
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
|
||||||
@@ -137,6 +133,67 @@ defmodule DynamicSupervisor do
|
|||||||
|
|
||||||
A supervisor is bound to the same name registration rules as a `GenServer`.
|
A supervisor is bound to the same name registration rules as a `GenServer`.
|
||||||
Read more about these rules in the documentation for `GenServer`.
|
Read more about these rules in the documentation for `GenServer`.
|
||||||
|
|
||||||
|
## Migrating from Supervisor's :simple_one_for_one
|
||||||
|
|
||||||
|
In case you were using the deprecated `:simple_one_for_one` strategy from
|
||||||
|
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
|
||||||
|
few steps.
|
||||||
|
|
||||||
|
Imagine the given "old" code:
|
||||||
|
|
||||||
|
defmodule MySupervisor do
|
||||||
|
use Supervisor
|
||||||
|
|
||||||
|
def start_link(init_arg) do
|
||||||
|
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_child(foo, bar, baz) do
|
||||||
|
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
|
||||||
|
Supervisor.start_child(__MODULE__, [foo, bar, baz])
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(init_arg) do
|
||||||
|
children = [
|
||||||
|
# Or the deprecated: worker(MyWorker, [init_arg])
|
||||||
|
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
|
||||||
|
]
|
||||||
|
|
||||||
|
Supervisor.init(children, strategy: :simple_one_for_one)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
It can be upgraded to the DynamicSupervisor like this:
|
||||||
|
|
||||||
|
defmodule MySupervisor do
|
||||||
|
use DynamicSupervisor
|
||||||
|
|
||||||
|
def start_link(init_arg) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_child(foo, bar, baz) do
|
||||||
|
# If MyWorker is not using the new child specs, we need to pass a map:
|
||||||
|
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
|
||||||
|
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
|
||||||
|
DynamicSupervisor.start_child(__MODULE__, spec)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(init_arg) do
|
||||||
|
DynamicSupervisor.init(
|
||||||
|
strategy: :one_for_one,
|
||||||
|
extra_arguments: [init_arg]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
The difference is that the `DynamicSupervisor` expects the child specification
|
||||||
|
at the moment `start_child/2` is called, and no longer on the init callback.
|
||||||
|
If there are any initial arguments given on initialization, such as `[initial_arg]`,
|
||||||
|
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@behaviour GenServer
|
@behaviour GenServer
|
||||||
@@ -158,7 +215,10 @@ defmodule DynamicSupervisor do
|
|||||||
extra_arguments: [term()]
|
extra_arguments: [term()]
|
||||||
}
|
}
|
||||||
|
|
||||||
@typedoc "Options given to `start_link/1` and `init/1` functions"
|
@typedoc "Options given to `start_link` functions"
|
||||||
|
@type option :: GenServer.option()
|
||||||
|
|
||||||
|
@typedoc "Options given to `start_link` and `init/1` functions"
|
||||||
@type init_option ::
|
@type init_option ::
|
||||||
{:strategy, strategy()}
|
{:strategy, strategy()}
|
||||||
| {:max_restarts, non_neg_integer()}
|
| {:max_restarts, non_neg_integer()}
|
||||||
@@ -169,15 +229,7 @@ defmodule DynamicSupervisor do
|
|||||||
@typedoc "Supported strategies"
|
@typedoc "Supported strategies"
|
||||||
@type strategy :: :one_for_one
|
@type strategy :: :one_for_one
|
||||||
|
|
||||||
@typedoc """
|
@typedoc "Return values of `start_child` functions"
|
||||||
Return values of `start_child` functions.
|
|
||||||
|
|
||||||
Unlike `Supervisor`, this module ignores the child spec ids,
|
|
||||||
so `{:error, {:already_started, pid}}` is not returned for child specs
|
|
||||||
given with the same id. `{:error, {:already_started, pid}}` is returned
|
|
||||||
however if a duplicate name is used when using
|
|
||||||
[name registration](`m:GenServer#module-name-registration`).
|
|
||||||
"""
|
|
||||||
@type on_start_child ::
|
@type on_start_child ::
|
||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
| {:ok, pid, info :: term}
|
| {:ok, pid, info :: term}
|
||||||
@@ -206,7 +258,6 @@ defmodule DynamicSupervisor do
|
|||||||
See `Supervisor` for more information about child specifications.
|
See `Supervisor` for more information about child specifications.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.6.1"
|
@doc since: "1.6.1"
|
||||||
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
|
|
||||||
def child_spec(options) when is_list(options) do
|
def child_spec(options) when is_list(options) do
|
||||||
id =
|
id =
|
||||||
case Keyword.get(options, :name, DynamicSupervisor) do
|
case Keyword.get(options, :name, DynamicSupervisor) do
|
||||||
@@ -226,7 +277,7 @@ defmodule DynamicSupervisor do
|
|||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
quote location: :keep, bind_quoted: [opts: opts] do
|
quote location: :keep, bind_quoted: [opts: opts] do
|
||||||
@behaviour DynamicSupervisor
|
@behaviour DynamicSupervisor
|
||||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a specification to start this module under a supervisor.
|
Returns a specification to start this module under a supervisor.
|
||||||
|
|
||||||
@@ -294,11 +345,9 @@ defmodule DynamicSupervisor do
|
|||||||
specified in the child spec given to `start_child/2`. Defaults to
|
specified in the child spec given to `start_child/2`. Defaults to
|
||||||
an empty list.
|
an empty list.
|
||||||
|
|
||||||
* Any of the standard [GenServer options](`t:GenServer.option/0`)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.6.0"
|
@doc since: "1.6.0"
|
||||||
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
|
@spec start_link([option | init_option]) :: Supervisor.on_start()
|
||||||
def start_link(options) when is_list(options) do
|
def start_link(options) when is_list(options) do
|
||||||
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
|
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
|
||||||
{sup_opts, start_opts} = Keyword.split(options, keys)
|
{sup_opts, start_opts} = Keyword.split(options, keys)
|
||||||
@@ -332,15 +381,9 @@ defmodule DynamicSupervisor do
|
|||||||
Note that a supervisor started with this function is linked to the parent
|
Note that a supervisor started with this function is linked to the parent
|
||||||
process and exits not only on crashes but also if the parent process exits
|
process and exits not only on crashes but also if the parent process exits
|
||||||
with `:normal` reason.
|
with `:normal` reason.
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
|
|
||||||
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
|
|
||||||
callback.
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.6.0"
|
@doc since: "1.6.0"
|
||||||
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
|
@spec start_link(module, term, [option]) :: Supervisor.on_start()
|
||||||
def start_link(module, init_arg, opts \\ []) do
|
def start_link(module, init_arg, opts \\ []) do
|
||||||
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
|
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
|
||||||
end
|
end
|
||||||
@@ -348,17 +391,11 @@ defmodule DynamicSupervisor do
|
|||||||
@doc """
|
@doc """
|
||||||
Dynamically adds a child specification to `supervisor` and starts that child.
|
Dynamically adds a child specification to `supervisor` and starts that child.
|
||||||
|
|
||||||
`child_spec` should be a valid [child specification](`m:Supervisor#module-child-specification`).
|
`child_spec` should be a valid child specification as detailed in the
|
||||||
The child process will be started as defined in the child specification. Note that while
|
"Child specification" section of the documentation for `Supervisor`. The child
|
||||||
|
process will be started as defined in the child specification. Note that while
|
||||||
the `:id` field is still required in the spec, the value is ignored and
|
the `:id` field is still required in the spec, the value is ignored and
|
||||||
therefore does not need to be unique. Unlike `Supervisor`, this module does not
|
therefore does not need to be unique.
|
||||||
return `{:error, {:already_started, pid}}` for child specs given with the same id.
|
|
||||||
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
|
|
||||||
used when using [name registration](`m:GenServer#module-name-registration`).
|
|
||||||
|
|
||||||
This function will block the `DynamicSupervisor` until the child initializes.
|
|
||||||
When starting too many processes dynamically, you may want to use a
|
|
||||||
`PartitionSupervisor` to split the work across multiple processes.
|
|
||||||
|
|
||||||
If the child process start function returns `{:ok, child}` or `{:ok, child,
|
If the child process start function returns `{:ok, child}` or `{:ok, child,
|
||||||
info}`, then child specification and PID are added to the supervisor and
|
info}`, then child specification and PID are added to the supervisor and
|
||||||
@@ -463,14 +500,6 @@ defmodule DynamicSupervisor do
|
|||||||
@doc """
|
@doc """
|
||||||
Terminates the given child identified by `pid`.
|
Terminates the given child identified by `pid`.
|
||||||
|
|
||||||
This function will block the `DynamicSupervisor` until the child
|
|
||||||
terminates, which may take an arbitrary amount of time if the child
|
|
||||||
is trapping exits and implements its own terminate callback.
|
|
||||||
For this reason, it is often better to ask the child process
|
|
||||||
itself to terminate, often by declaring in its child spec it has
|
|
||||||
a restart strategy of `:transient` (or `:temporary`) and then
|
|
||||||
sending it a message to stop with reason `:shutdown`.
|
|
||||||
|
|
||||||
If successful, this function returns `:ok`. If there is no process with
|
If successful, this function returns `:ok`. If there is no process with
|
||||||
the given PID, this function returns `{:error, :not_found}`.
|
the given PID, this function returns `{:error, :not_found}`.
|
||||||
"""
|
"""
|
||||||
@@ -481,11 +510,11 @@ defmodule DynamicSupervisor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list with information about all children of the given supervisor.
|
Returns a list with information about all children.
|
||||||
|
|
||||||
Note that calling this function when supervising a large number
|
Note that calling this function when supervising a large number
|
||||||
of children under low memory conditions can bring the system down due to an
|
of children under low memory conditions can cause an out of memory
|
||||||
out of memory error.
|
exception.
|
||||||
|
|
||||||
This function returns a list of tuples containing:
|
This function returns a list of tuples containing:
|
||||||
|
|
||||||
@@ -1107,6 +1136,15 @@ defmodule DynamicSupervisor do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def format_status(:terminate, [_pdict, state]) do
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_status(_, [_pdict, %{mod: mod} = state]) do
|
||||||
|
[data: [{~c"State", state}], supervisor: [{~c"Callback", mod}]]
|
||||||
|
end
|
||||||
|
|
||||||
## Helpers
|
## Helpers
|
||||||
|
|
||||||
@compile {:inline, call: 2}
|
@compile {:inline, call: 2}
|
||||||
|
|||||||
+271
-686
File diff suppressed because it is too large
Load Diff
+239
-1139
File diff suppressed because it is too large
Load Diff
+116
-698
File diff suppressed because it is too large
Load Diff
+13
-17
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
require Record
|
require Record
|
||||||
|
|
||||||
defmodule File.Stat do
|
defmodule File.Stat do
|
||||||
@@ -64,19 +60,19 @@ defmodule File.Stat do
|
|||||||
defstruct keys
|
defstruct keys
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
size: non_neg_integer() | :undefined,
|
size: non_neg_integer(),
|
||||||
type: :device | :directory | :regular | :other | :symlink | :undefined,
|
type: :device | :directory | :regular | :other | :symlink,
|
||||||
access: :read | :write | :read_write | :none | :undefined,
|
access: :read | :write | :read_write | :none,
|
||||||
atime: :calendar.datetime() | integer() | :undefined,
|
atime: :calendar.datetime() | integer(),
|
||||||
mtime: :calendar.datetime() | integer() | :undefined,
|
mtime: :calendar.datetime() | integer(),
|
||||||
ctime: :calendar.datetime() | integer() | :undefined,
|
ctime: :calendar.datetime() | integer(),
|
||||||
mode: non_neg_integer() | :undefined,
|
mode: non_neg_integer(),
|
||||||
links: non_neg_integer() | :undefined,
|
links: non_neg_integer(),
|
||||||
major_device: non_neg_integer() | :undefined,
|
major_device: non_neg_integer(),
|
||||||
minor_device: non_neg_integer() | :undefined,
|
minor_device: non_neg_integer(),
|
||||||
inode: non_neg_integer() | :undefined,
|
inode: non_neg_integer(),
|
||||||
uid: non_neg_integer() | :undefined,
|
uid: non_neg_integer(),
|
||||||
gid: non_neg_integer() | :undefined
|
gid: non_neg_integer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule File.Stream do
|
defmodule File.Stream do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Defines a `File.Stream` struct returned by `File.stream!/3`.
|
Defines a `File.Stream` struct returned by `File.stream!/3`.
|
||||||
@@ -18,22 +14,10 @@ defmodule File.Stream do
|
|||||||
|
|
||||||
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
|
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{}
|
||||||
path: Path.t(),
|
|
||||||
modes: [term()],
|
|
||||||
line_or_bytes: :line | pos_integer(),
|
|
||||||
raw: boolean(),
|
|
||||||
node: node()
|
|
||||||
}
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def __build__(path, line_or_bytes, modes) do
|
def __build__(path, modes, line_or_bytes) do
|
||||||
with {:read_offset, offset} <- :lists.keyfind(:read_offset, 1, modes),
|
|
||||||
false <- is_integer(offset) and offset >= 0 do
|
|
||||||
raise ArgumentError,
|
|
||||||
"expected :read_offset to be a non-negative integer, got: #{inspect(offset)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
raw = :lists.keyfind(:encoding, 1, modes) == false
|
raw = :lists.keyfind(:encoding, 1, modes) == false
|
||||||
|
|
||||||
modes =
|
modes =
|
||||||
@@ -104,7 +88,7 @@ defmodule File.Stream do
|
|||||||
start_fun = fn ->
|
start_fun = fn ->
|
||||||
case File.Stream.__open__(stream, read_modes(modes)) do
|
case File.Stream.__open__(stream, read_modes(modes)) do
|
||||||
{:ok, device} ->
|
{:ok, device} ->
|
||||||
skip_bom_and_offset(device, raw, modes)
|
if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: device
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
raise File.Error, reason: reason, action: "stream", path: stream.path
|
raise File.Error, reason: reason, action: "stream", path: stream.path
|
||||||
@@ -120,14 +104,9 @@ defmodule File.Stream do
|
|||||||
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
|
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
def count(%{modes: modes, line_or_bytes: :line, path: path, raw: raw} = stream) do
|
def count(%{modes: modes, line_or_bytes: :line, path: path} = stream) do
|
||||||
pattern = :binary.compile_pattern("\n")
|
pattern = :binary.compile_pattern("\n")
|
||||||
|
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
|
||||||
counter = fn device ->
|
|
||||||
device = skip_bom_and_offset(device, raw, modes)
|
|
||||||
count_lines(device, path, pattern, read_function(stream), 0, :empty)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, open!(stream, modes, counter)}
|
{:ok, open!(stream, modes, counter)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,11 +116,8 @@ defmodule File.Stream do
|
|||||||
{:error, __MODULE__}
|
{:error, __MODULE__}
|
||||||
|
|
||||||
{:ok, %{size: size}} ->
|
{:ok, %{size: size}} ->
|
||||||
bom_offset = count_raw_bom(stream, modes)
|
|
||||||
offset = get_read_offset(modes)
|
|
||||||
size = max(size - bom_offset - offset, 0)
|
|
||||||
remainder = if rem(size, bytes) == 0, do: 0, else: 1
|
remainder = if rem(size, bytes) == 0, do: 0, else: 1
|
||||||
{:ok, div(size, bytes) + remainder}
|
{:ok, div(size, bytes) + remainder - count_raw_bom(stream, modes)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
raise File.Error, reason: reason, action: "stream", path: path
|
raise File.Error, reason: reason, action: "stream", path: path
|
||||||
@@ -182,23 +158,6 @@ defmodule File.Stream do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp skip_bom_and_offset(device, raw, modes) do
|
|
||||||
device =
|
|
||||||
if :trim_bom in modes do
|
|
||||||
device |> trim_bom(raw) |> elem(0)
|
|
||||||
else
|
|
||||||
device
|
|
||||||
end
|
|
||||||
|
|
||||||
offset = get_read_offset(modes)
|
|
||||||
|
|
||||||
if offset > 0 do
|
|
||||||
{:ok, _} = :file.position(device, {:cur, offset})
|
|
||||||
end
|
|
||||||
|
|
||||||
device
|
|
||||||
end
|
|
||||||
|
|
||||||
defp trim_bom(device, true) do
|
defp trim_bom(device, true) do
|
||||||
bom_length = device |> IO.binread(4) |> bom_length()
|
bom_length = device |> IO.binread(4) |> bom_length()
|
||||||
{:ok, new_pos} = :file.position(device, bom_length)
|
{:ok, new_pos} = :file.position(device, bom_length)
|
||||||
@@ -224,39 +183,25 @@ defmodule File.Stream do
|
|||||||
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
|
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
|
||||||
defp bom_length(_binary), do: 0
|
defp bom_length(_binary), do: 0
|
||||||
|
|
||||||
def get_read_offset(modes) do
|
|
||||||
case :lists.keyfind(:read_offset, 1, modes) do
|
|
||||||
{:read_offset, offset} -> offset
|
|
||||||
false -> 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp read_modes(modes) do
|
defp read_modes(modes) do
|
||||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||||
end
|
end
|
||||||
|
|
||||||
defp count_lines(device, path, pattern, read, count, last_byte) do
|
defp count_lines(device, path, pattern, read, count) do
|
||||||
case read.(device) do
|
case read.(device) do
|
||||||
data when is_binary(data) and byte_size(data) > 0 ->
|
|
||||||
newlines = length(:binary.matches(data, pattern))
|
|
||||||
last = :binary.last(data)
|
|
||||||
count_lines(device, path, pattern, read, count + newlines, last)
|
|
||||||
|
|
||||||
data when is_binary(data) ->
|
data when is_binary(data) ->
|
||||||
count_lines(device, path, pattern, read, count, last_byte)
|
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
|
||||||
|
|
||||||
:eof ->
|
:eof ->
|
||||||
case last_byte do
|
count
|
||||||
:empty -> 0
|
|
||||||
?\n -> count
|
|
||||||
_ -> count + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
raise File.Error, reason: reason, action: "stream", path: path
|
raise File.Error, reason: reason, action: "stream", path: path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
|
||||||
|
|
||||||
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
||||||
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
||||||
end
|
end
|
||||||
|
|||||||
+147
-231
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
import Kernel, except: [round: 1]
|
import Kernel, except: [round: 1]
|
||||||
|
|
||||||
defmodule Float do
|
defmodule Float do
|
||||||
@@ -25,7 +21,7 @@ defmodule Float do
|
|||||||
and arithmetic due to the fact most decimal fractions cannot be
|
and arithmetic due to the fact most decimal fractions cannot be
|
||||||
represented by a floating-point binary and most operations are not exact,
|
represented by a floating-point binary and most operations are not exact,
|
||||||
but operate on approximations. Those issues are not specific
|
but operate on approximations. Those issues are not specific
|
||||||
to Elixir, they are a property of floating-point representation itself.
|
to Elixir, they are a property of floating point representation itself.
|
||||||
|
|
||||||
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
||||||
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
||||||
@@ -42,7 +38,7 @@ defmodule Float do
|
|||||||
|
|
||||||
To learn more about floating-point arithmetic visit:
|
To learn more about floating-point arithmetic visit:
|
||||||
|
|
||||||
* [0.30000000000000004.com](https://0.30000000000000004.com/)
|
* [0.30000000000000004.com](http://0.30000000000000004.com/)
|
||||||
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -65,7 +61,6 @@ defmodule Float do
|
|||||||
1.7976931348623157e308
|
1.7976931348623157e308
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec max_finite() :: float
|
|
||||||
def max_finite, do: @max_finite
|
def max_finite, do: @max_finite
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -77,7 +72,6 @@ defmodule Float do
|
|||||||
-1.7976931348623157e308
|
-1.7976931348623157e308
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec min_finite() :: float
|
|
||||||
def min_finite, do: @min_finite
|
def min_finite, do: @min_finite
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -143,8 +137,6 @@ defmodule Float do
|
|||||||
iex> Float.parse("56.5xyz")
|
iex> Float.parse("56.5xyz")
|
||||||
{56.5, "xyz"}
|
{56.5, "xyz"}
|
||||||
|
|
||||||
iex> Float.parse(".12")
|
|
||||||
:error
|
|
||||||
iex> Float.parse("pi")
|
iex> Float.parse("pi")
|
||||||
:error
|
:error
|
||||||
iex> Float.parse("1.7976931348623159e+308")
|
iex> Float.parse("1.7976931348623159e+308")
|
||||||
@@ -167,76 +159,44 @@ defmodule Float do
|
|||||||
parse_unsigned(binary)
|
parse_unsigned(binary)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
|
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
|
||||||
do: parse_mantissa(binary, rest, false)
|
do: parse_unsigned(rest, false, false, <<digit>>)
|
||||||
|
|
||||||
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
||||||
|
|
||||||
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
|
||||||
do: parse_mantissa(binary, rest, dot?)
|
do: parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
|
||||||
|
|
||||||
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
|
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
|
||||||
do: parse_mantissa(binary, rest, true)
|
do: parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
|
||||||
|
|
||||||
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
|
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
|
||||||
when exp_marker in ~c"eE" and digit in ?0..?9,
|
when exp_marker in ~c"eE" and digit in ?0..?9,
|
||||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
|
||||||
|
|
||||||
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
|
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
|
||||||
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
||||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
|
||||||
|
|
||||||
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)
|
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
|
||||||
|
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
|
||||||
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
# rescue the ArgumentError here and return an error.
|
||||||
do: parse_exponent(binary, exp_pos, rest, dot?)
|
defp parse_unsigned(rest, dot?, true = _e?, acc) do
|
||||||
|
:erlang.binary_to_float(add_dot(acc, dot?))
|
||||||
defp parse_exponent(binary, exp_pos, rest, dot?),
|
|
||||||
do: finish_exponent(binary, exp_pos, rest, dot?)
|
|
||||||
|
|
||||||
defp finish_mantissa(binary, rest, _dot? = true) do
|
|
||||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
|
||||||
rescue
|
rescue
|
||||||
ArgumentError -> :error
|
ArgumentError -> :error
|
||||||
|
else
|
||||||
|
float -> {float, rest}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Bare integer: * 1.0 casts to the nearest float without building a new binary,
|
defp parse_unsigned(rest, dot?, false = _e?, acc),
|
||||||
# and raises ArithmeticError on overflow (for example a 400-digit integer).
|
do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
|
||||||
defp finish_mantissa(binary, rest, _dot? = false) do
|
|
||||||
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
|
|
||||||
rescue
|
|
||||||
ArithmeticError -> :error
|
|
||||||
end
|
|
||||||
|
|
||||||
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
|
defp add_dot(acc, true), do: acc
|
||||||
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
|
defp add_dot(acc, false), do: acc <> ".0"
|
||||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
|
||||||
rescue
|
|
||||||
ArgumentError -> :error
|
|
||||||
end
|
|
||||||
|
|
||||||
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
|
|
||||||
# form a valid float literal.
|
|
||||||
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
|
|
||||||
len = byte_size(binary) - byte_size(rest)
|
|
||||||
|
|
||||||
literal =
|
|
||||||
IO.iodata_to_binary([
|
|
||||||
:binary.part(binary, 0, exp_pos),
|
|
||||||
".0",
|
|
||||||
:binary.part(binary, exp_pos, len - exp_pos)
|
|
||||||
])
|
|
||||||
|
|
||||||
{:erlang.binary_to_float(literal), rest}
|
|
||||||
rescue
|
|
||||||
ArgumentError -> :error
|
|
||||||
end
|
|
||||||
|
|
||||||
defp consumed(binary, ""), do: binary
|
|
||||||
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Rounds a float to the largest float less than or equal to `number`.
|
Rounds a float to the largest number less than or equal to `num`.
|
||||||
|
|
||||||
`floor/2` also accepts a precision to round a floating-point value down
|
`floor/2` also accepts a precision to round a floating-point value down
|
||||||
to an arbitrary number of fractional digits (between 0 and 15).
|
to an arbitrary number of fractional digits (between 0 and 15).
|
||||||
@@ -248,7 +208,7 @@ defmodule Float do
|
|||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
The behavior of `floor/2` for floats can be surprising. For example:
|
The behaviour of `floor/2` for floats can be surprising. For example:
|
||||||
|
|
||||||
iex> Float.floor(12.52, 2)
|
iex> Float.floor(12.52, 2)
|
||||||
12.51
|
12.51
|
||||||
@@ -256,7 +216,7 @@ defmodule Float do
|
|||||||
One may have expected it to floor to 12.52. This is not a bug.
|
One may have expected it to floor to 12.52. This is not a bug.
|
||||||
Most decimal fractions cannot be represented as a binary floating point
|
Most decimal fractions cannot be represented as a binary floating point
|
||||||
and therefore the number above is internally represented as 12.51999999,
|
and therefore the number above is internally represented as 12.51999999,
|
||||||
which explains the behavior above.
|
which explains the behaviour above.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -284,15 +244,15 @@ defmodule Float do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Rounds a float to the smallest float greater than or equal to `number`.
|
Rounds a float to the smallest integer greater than or equal to `num`.
|
||||||
|
|
||||||
`ceil/2` also accepts a precision to round a floating-point value up
|
`ceil/2` also accepts a precision to round a floating-point value down
|
||||||
to an arbitrary number of fractional digits (between 0 and 15).
|
to an arbitrary number of fractional digits (between 0 and 15).
|
||||||
|
|
||||||
The operation is performed on the binary floating point, without a
|
The operation is performed on the binary floating point, without a
|
||||||
conversion to decimal.
|
conversion to decimal.
|
||||||
|
|
||||||
The behavior of `ceil/2` for floats can be surprising. For example:
|
The behaviour of `ceil/2` for floats can be surprising. For example:
|
||||||
|
|
||||||
iex> Float.ceil(-12.52, 2)
|
iex> Float.ceil(-12.52, 2)
|
||||||
-12.51
|
-12.51
|
||||||
@@ -300,7 +260,7 @@ defmodule Float do
|
|||||||
One may have expected it to ceil to -12.52. This is not a bug.
|
One may have expected it to ceil to -12.52. This is not a bug.
|
||||||
Most decimal fractions cannot be represented as a binary floating point
|
Most decimal fractions cannot be represented as a binary floating point
|
||||||
and therefore the number above is internally represented as -12.51999999,
|
and therefore the number above is internally represented as -12.51999999,
|
||||||
which explains the behavior above.
|
which explains the behaviour above.
|
||||||
|
|
||||||
This function always returns floats. `Kernel.trunc/1` may be used instead to
|
This function always returns floats. `Kernel.trunc/1` may be used instead to
|
||||||
truncate the result to an integer afterwards.
|
truncate the result to an integer afterwards.
|
||||||
@@ -313,8 +273,6 @@ defmodule Float do
|
|||||||
-56.0
|
-56.0
|
||||||
iex> Float.ceil(34.251, 2)
|
iex> Float.ceil(34.251, 2)
|
||||||
34.26
|
34.26
|
||||||
iex> Float.ceil(-0.01)
|
|
||||||
-0.0
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec ceil(float, precision_range) :: float
|
@spec ceil(float, precision_range) :: float
|
||||||
@@ -345,7 +303,7 @@ defmodule Float do
|
|||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
The behavior of `round/2` for floats can be surprising. For example:
|
The behaviour of `round/2` for floats can be surprising. For example:
|
||||||
|
|
||||||
iex> Float.round(5.5675, 3)
|
iex> Float.round(5.5675, 3)
|
||||||
5.567
|
5.567
|
||||||
@@ -353,9 +311,9 @@ defmodule Float do
|
|||||||
One may have expected it to round to the half up 5.568. This is not a bug.
|
One may have expected it to round to the half up 5.568. This is not a bug.
|
||||||
Most decimal fractions cannot be represented as a binary floating point
|
Most decimal fractions cannot be represented as a binary floating point
|
||||||
and therefore the number above is internally represented as 5.567499999,
|
and therefore the number above is internally represented as 5.567499999,
|
||||||
which explains the behavior above. If you want exact rounding for decimals,
|
which explains the behaviour above. If you want exact rounding for decimals,
|
||||||
you must use a decimal library. The behavior above is also in accordance
|
you must use a decimal library. The behaviour above is also in accordance
|
||||||
with reference implementations, such as "Correctly Rounded Binary-Decimal and
|
to reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||||
Decimal-Binary Conversions" by David M. Gay.
|
Decimal-Binary Conversions" by David M. Gay.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -372,20 +330,16 @@ defmodule Float do
|
|||||||
-6.0
|
-6.0
|
||||||
iex> Float.round(12.341444444444441, 15)
|
iex> Float.round(12.341444444444441, 15)
|
||||||
12.341444444444441
|
12.341444444444441
|
||||||
iex> Float.round(-0.01)
|
|
||||||
-0.0
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec round(float, precision_range) :: float
|
@spec round(float, precision_range) :: float
|
||||||
|
# This implementation is slow since it relies on big integers.
|
||||||
|
# Faster implementations are available on more recent papers
|
||||||
|
# and could be implemented in the future.
|
||||||
def round(float, precision \\ 0)
|
def round(float, precision \\ 0)
|
||||||
|
|
||||||
def round(float, 0) when float === 0.0 or float === -0.0, do: float
|
|
||||||
|
|
||||||
def round(float, 0) when is_float(float) do
|
def round(float, 0) when is_float(float) do
|
||||||
case :erlang.round(float) * 1.0 do
|
float |> :erlang.round() |> :erlang.float()
|
||||||
zero when zero == 0.0 and float < 0.0 -> -0.0
|
|
||||||
rounded -> rounded
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def round(float, precision) when is_float(float) and precision in @precision_range do
|
def round(float, precision) when is_float(float) and precision in @precision_range do
|
||||||
@@ -396,170 +350,130 @@ defmodule Float do
|
|||||||
raise ArgumentError, invalid_precision_message(precision)
|
raise ArgumentError, invalid_precision_message(precision)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Decimal-place rounding via exact rational scaling. This is the bignum
|
defp round(0.0 = num, _precision, _rounding), do: num
|
||||||
# core used by reference implementations like David M. Gay's "Correctly
|
|
||||||
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
|
|
||||||
# @doc above), Python's round(), and Java's BigDecimal.setScale.
|
|
||||||
#
|
|
||||||
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
|
|
||||||
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
|
|
||||||
# Because precision is bounded to 0..15, the product fits in ~103 bits
|
|
||||||
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
|
|
||||||
# directly without approximation.
|
|
||||||
# 3. Round the exact rational to an integer per the requested mode
|
|
||||||
# (half_up / floor / ceil) using quotient and remainder.
|
|
||||||
# 4. Emit the float closest to rounded_int / 10^precision:
|
|
||||||
# - fast path: when rounded_int < 2^53, both operands are exactly
|
|
||||||
# representable as floats and IEEE division is correctly rounded.
|
|
||||||
# - slow path: bignum alignment + manual mantissa extraction with
|
|
||||||
# round-to-nearest-even for the trailing bit.
|
|
||||||
#
|
|
||||||
# The integer-rounding decision (step 3) and the binary-emission decision
|
|
||||||
# (step 4) are deliberately independent: step 3 picks the exact rational
|
|
||||||
# the user asked for, step 4 picks the closest float to that rational.
|
|
||||||
# Conflating them is the classic source of double-rounding bugs.
|
|
||||||
#
|
|
||||||
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
|
|
||||||
# for round-trip printing) but target different problems or assume
|
|
||||||
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
|
|
||||||
# At precision <= 15, the exact path is small, easy to audit, and fast
|
|
||||||
# enough that a more complex algorithm has not been justified by benchmarks.
|
|
||||||
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
|
|
||||||
|
|
||||||
defp round(float, precision, mode) do
|
defp round(float, precision, rounding) do
|
||||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||||
|
{num, count} = decompose(significant, 1)
|
||||||
|
count = count - exp + 1023
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
|
# Precision beyond 15 digits
|
||||||
# to 10^-precision; everything else rounds to signed zero).
|
count >= 104 ->
|
||||||
exp == 0 ->
|
case rounding do
|
||||||
tiny_round(sign, precision, mode)
|
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||||
|
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||||
|
_ -> 0.0
|
||||||
|
end
|
||||||
|
|
||||||
# |float| >= 2^52 — has no fractional bits, return unchanged.
|
# We are asking more precision than we have
|
||||||
exp - 1075 >= 0 ->
|
count <= precision ->
|
||||||
float
|
float
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
mantissa = @power_of_2_to_52 ||| mantissa
|
# Difference in precision between float and asked precision
|
||||||
shift = 1075 - exp
|
# We subtract 1 because we need to calculate the remainder too
|
||||||
do_round(sign, mantissa, shift, precision, mode)
|
diff = count - precision - 1
|
||||||
|
|
||||||
|
# Get up to latest so we calculate the remainder
|
||||||
|
power_of_10 = power_of_10(diff)
|
||||||
|
|
||||||
|
# Convert the numerand to decimal base
|
||||||
|
num = num * power_of_5(count)
|
||||||
|
|
||||||
|
# Move to the given precision - 1
|
||||||
|
num = div(num, power_of_10)
|
||||||
|
div = div(num, 10)
|
||||||
|
num = rounding(rounding, sign, num, div)
|
||||||
|
|
||||||
|
# Convert back to float without loss
|
||||||
|
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
|
||||||
|
den = power_of_10(precision)
|
||||||
|
boundary = den <<< 52
|
||||||
|
|
||||||
|
cond do
|
||||||
|
num == 0 ->
|
||||||
|
0.0
|
||||||
|
|
||||||
|
num >= boundary ->
|
||||||
|
{den, exp} = scale_down(num, boundary, 52)
|
||||||
|
decimal_to_float(sign, num, den, exp)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{num, exp} = scale_up(num, boundary, 52)
|
||||||
|
decimal_to_float(sign, num, den, exp)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
|
defp decompose(significant, initial) do
|
||||||
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
|
decompose(significant, 1, 0, initial)
|
||||||
tiny_round(sign, precision, mode)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_round(sign, mantissa, shift, precision, mode) do
|
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||||
power = power_of_10(precision)
|
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||||
product = mantissa * power
|
end
|
||||||
half = 1 <<< (shift - 1)
|
|
||||||
quotient = product >>> shift
|
|
||||||
remainder = product - (quotient <<< shift)
|
|
||||||
rounded_int = round_step(mode, sign, quotient, remainder, half)
|
|
||||||
|
|
||||||
cond do
|
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||||
rounded_int == 0 ->
|
decompose(bits, count + 1, last_count, acc)
|
||||||
signed_zero(sign)
|
end
|
||||||
|
|
||||||
rounded_int < @power_of_2_to_52 <<< 1 ->
|
defp decompose(<<>>, _count, last_count, acc) do
|
||||||
# Both rounded_int and power fit in 53 bits, so IEEE float division
|
{acc, last_count}
|
||||||
# is correctly rounded.
|
end
|
||||||
result = rounded_int / power
|
|
||||||
if sign == 1, do: -result, else: result
|
|
||||||
|
|
||||||
true ->
|
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
|
||||||
bignum_to_float(sign, rounded_int, power)
|
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
|
||||||
|
|
||||||
|
defp scale_down(num, den, exp) do
|
||||||
|
new_den = den <<< 1
|
||||||
|
|
||||||
|
if num < new_den do
|
||||||
|
{den >>> 52, exp}
|
||||||
|
else
|
||||||
|
scale_down(num, new_den, exp + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp round_step(:half_up, _sign, quotient, remainder, half) do
|
defp decimal_to_float(sign, num, den, exp) do
|
||||||
if remainder >= half, do: quotient + 1, else: quotient
|
quo = div(num, den)
|
||||||
end
|
rem = num - quo * den
|
||||||
|
|
||||||
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
|
tmp =
|
||||||
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
case den >>> 1 do
|
||||||
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
|
den when rem > den -> quo + 1
|
||||||
|
den when rem < den -> quo
|
||||||
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
_ when (quo &&& 1) === 1 -> quo + 1
|
||||||
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
|
_ -> quo
|
||||||
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
|
|
||||||
|
|
||||||
defp signed_zero(0), do: 0.0
|
|
||||||
defp signed_zero(1), do: -0.0
|
|
||||||
|
|
||||||
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
|
|
||||||
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
|
|
||||||
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
|
|
||||||
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
|
|
||||||
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
|
|
||||||
|
|
||||||
# Slow path: emit float closest to `sign * rounded_int / power` when
|
|
||||||
# rounded_int >= 2^53. The binary emission step is always IEEE
|
|
||||||
# round-to-nearest-even, regardless of the integer-rounding mode.
|
|
||||||
defp bignum_to_float(sign, rounded_int, power) do
|
|
||||||
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
|
|
||||||
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
|
|
||||||
|
|
||||||
quotient = div(numerator, denominator)
|
|
||||||
remainder = numerator - quotient * denominator
|
|
||||||
half = denominator >>> 1
|
|
||||||
|
|
||||||
mantissa =
|
|
||||||
cond do
|
|
||||||
remainder > half -> quotient + 1
|
|
||||||
remainder < half -> quotient
|
|
||||||
(quotient &&& 1) === 1 -> quotient + 1
|
|
||||||
true -> quotient
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
|
tmp = tmp - @power_of_2_to_52
|
||||||
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
|
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
|
||||||
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
|
tmp
|
||||||
# range so the 52-bit packing below doesn't silently truncate.
|
|
||||||
{mantissa, exp} =
|
|
||||||
if mantissa == @power_of_2_to_52 <<< 1,
|
|
||||||
do: {@power_of_2_to_52, exp + 1},
|
|
||||||
else: {mantissa, exp}
|
|
||||||
|
|
||||||
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
|
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
|
defp rounding(:floor, 1, _num, div), do: div + 1
|
||||||
# and the resulting float = numerator/denominator * 2^(exp-52).
|
defp rounding(:ceil, 0, _num, div), do: div + 1
|
||||||
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
|
|
||||||
new_power = power <<< shift_adjust
|
|
||||||
|
|
||||||
if rounded_int < new_power <<< 53,
|
defp rounding(:half_up, _sign, num, div) do
|
||||||
do: {rounded_int, new_power, 52 + shift_adjust},
|
case rem(num, 10) do
|
||||||
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
|
rem when rem < 5 -> div
|
||||||
end
|
rem when rem >= 5 -> div + 1
|
||||||
|
|
||||||
defp align(rounded_int, power, shift_adjust) do
|
|
||||||
shifted = rounded_int <<< -shift_adjust
|
|
||||||
|
|
||||||
cond do
|
|
||||||
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
|
|
||||||
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
|
|
||||||
true -> {shifted <<< 1, power, 51 + shift_adjust}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp bit_length(0), do: 0
|
defp rounding(_, _, _, div), do: div
|
||||||
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
|
|
||||||
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
|
|
||||||
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
|
|
||||||
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
|
|
||||||
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
|
|
||||||
defp bit_length(_integer, acc), do: acc
|
|
||||||
|
|
||||||
Enum.reduce(0..15, 1, fn exponent, acc ->
|
Enum.reduce(0..104, 1, fn x, acc ->
|
||||||
defp power_of_10(unquote(exponent)), do: unquote(acc)
|
defp power_of_10(unquote(x)), do: unquote(acc)
|
||||||
acc * 10
|
acc * 10
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Enum.reduce(0..104, 1, fn x, acc ->
|
||||||
|
defp power_of_5(unquote(x)), do: unquote(acc)
|
||||||
|
acc * 5
|
||||||
|
end)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a pair of integers whose ratio is exactly equal
|
Returns a pair of integers whose ratio is exactly equal
|
||||||
to the original float and with a positive denominator.
|
to the original float and with a positive denominator.
|
||||||
@@ -584,7 +498,7 @@ defmodule Float do
|
|||||||
"""
|
"""
|
||||||
@doc since: "1.4.0"
|
@doc since: "1.4.0"
|
||||||
@spec ratio(float) :: {integer, pos_integer}
|
@spec ratio(float) :: {integer, pos_integer}
|
||||||
def ratio(float) when is_float(float) and float == 0.0, do: {0, 1}
|
def ratio(0.0), do: {0, 1}
|
||||||
|
|
||||||
def ratio(float) when is_float(float) do
|
def ratio(float) when is_float(float) do
|
||||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||||
@@ -640,23 +554,27 @@ defmodule Float do
|
|||||||
Returns a charlist which corresponds to the shortest text representation
|
Returns a charlist which corresponds to the shortest text representation
|
||||||
of the given float.
|
of the given float.
|
||||||
|
|
||||||
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
|
The underlying algorithm changes depending on the Erlang/OTP version:
|
||||||
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
|
|
||||||
Design and Implementation.
|
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
|
||||||
|
float-to-string conversion" in Proceedings of the SIGPLAN '2018
|
||||||
|
Conference on Programming Language Design and Implementation.
|
||||||
|
|
||||||
|
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
|
||||||
|
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
|
||||||
|
Conference on Programming Language Design and Implementation.
|
||||||
|
|
||||||
For a configurable representation, use `:erlang.float_to_list/2`.
|
For a configurable representation, use `:erlang.float_to_list/2`.
|
||||||
|
|
||||||
Inlined by the compiler.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Float.to_charlist(7.0)
|
iex> Float.to_charlist(7.0)
|
||||||
~c"7.0"
|
'7.0'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_charlist(float) :: charlist
|
@spec to_charlist(float) :: charlist
|
||||||
def to_charlist(float) do
|
def to_charlist(float) when is_float(float) do
|
||||||
:erlang.float_to_list(float, [:short])
|
:io_lib_format.fwrite_g(float)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -675,8 +593,6 @@ defmodule Float do
|
|||||||
|
|
||||||
For a configurable representation, use `:erlang.float_to_binary/2`.
|
For a configurable representation, use `:erlang.float_to_binary/2`.
|
||||||
|
|
||||||
Inlined by the compiler.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> Float.to_string(7.0)
|
iex> Float.to_string(7.0)
|
||||||
@@ -684,8 +600,8 @@ defmodule Float do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec to_string(float) :: String.t()
|
@spec to_string(float) :: String.t()
|
||||||
def to_string(float) do
|
def to_string(float) when is_float(float) do
|
||||||
:erlang.float_to_binary(float, [:short])
|
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@@ -705,7 +621,7 @@ defmodule Float do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp invalid_precision_message(precision) do
|
defp invalid_precision_message(precision) do
|
||||||
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}"
|
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
|
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule Function do
|
defmodule Function do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A set of functions for working with functions.
|
A set of functions for working with functions.
|
||||||
@@ -69,6 +65,7 @@ defmodule Function do
|
|||||||
| :name
|
| :name
|
||||||
| :new_index
|
| :new_index
|
||||||
| :new_uniq
|
| :new_uniq
|
||||||
|
| :pid
|
||||||
| :type
|
| :type
|
||||||
| :uniq
|
| :uniq
|
||||||
|
|
||||||
@@ -111,6 +108,8 @@ defmodule Function do
|
|||||||
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
||||||
additional keys are returned:
|
additional keys are returned:
|
||||||
|
|
||||||
|
* `:pid` - PID of the process that originally created the function.
|
||||||
|
|
||||||
* `:index` - (integer) an index into the module function table.
|
* `:index` - (integer) an index into the module function table.
|
||||||
|
|
||||||
* `:new_index` - (integer) an index into the module function table.
|
* `:new_index` - (integer) an index into the module function table.
|
||||||
@@ -156,7 +155,7 @@ defmodule Function do
|
|||||||
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
||||||
|
|
||||||
For anonymous functions, there is also information about any of the
|
For anonymous functions, there is also information about any of the
|
||||||
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`.
|
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
|
||||||
For a named function, the value of any of these items is always the
|
For a named function, the value of any of these items is always the
|
||||||
atom `:undefined`.
|
atom `:undefined`.
|
||||||
|
|
||||||
@@ -176,6 +175,8 @@ defmodule Function do
|
|||||||
iex> fun = &String.length/1
|
iex> fun = &String.length/1
|
||||||
iex> Function.info(fun, :name)
|
iex> Function.info(fun, :name)
|
||||||
{:name, :length}
|
{:name, :length}
|
||||||
|
iex> Function.info(fun, :pid)
|
||||||
|
{:pid, :undefined}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@doc since: "1.7.0"
|
@doc since: "1.7.0"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule GenEvent do
|
defmodule GenEvent do
|
||||||
# Functions from this module are deprecated in elixir_dispatch.
|
# Functions from this module are deprecated in elixir_dispatch.
|
||||||
|
|
||||||
@@ -23,10 +19,10 @@ defmodule GenEvent do
|
|||||||
One alternative to GenEvent is a very minimal solution consisting of using a
|
One alternative to GenEvent is a very minimal solution consisting of using a
|
||||||
supervisor and multiple GenServers started under it. The supervisor acts as
|
supervisor and multiple GenServers started under it. The supervisor acts as
|
||||||
the "event manager" and the children GenServers act as the "event handlers".
|
the "event manager" and the children GenServers act as the "event handlers".
|
||||||
This approach has some shortcomings (it provides no back-pressure for example)
|
This approach has some shortcomings (it provides no backpressure for example)
|
||||||
but can still replace GenEvent for low-profile usages of it. [This blog post
|
but can still replace GenEvent for low-profile usages of it. [This blog post
|
||||||
by José
|
by José
|
||||||
Valim](https://dashbit.co/blog/replacing-genevent-by-a-supervisor-plus-genserver)
|
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
|
||||||
has more detailed information on this approach.
|
has more detailed information on this approach.
|
||||||
|
|
||||||
### GenStage
|
### GenStage
|
||||||
@@ -35,8 +31,8 @@ defmodule GenEvent do
|
|||||||
[GenStage](https://github.com/elixir-lang/gen_stage) provides a great
|
[GenStage](https://github.com/elixir-lang/gen_stage) provides a great
|
||||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||||
team; it provides a tool to implement systems that exchange events in a
|
team; it provides a tool to implement systems that exchange events in a
|
||||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
demand-driven way with built-in support for backpressure. See the [GenStage
|
||||||
documentation](https://gen-stage.hexdocs.pm) for more information.
|
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||||
|
|
||||||
### `:gen_event`
|
### `:gen_event`
|
||||||
|
|
||||||
@@ -50,8 +46,8 @@ defmodule GenEvent do
|
|||||||
@callback init(args :: term) ::
|
@callback init(args :: term) ::
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
| {:ok, state, :hibernate}
|
| {:ok, state, :hibernate}
|
||||||
| {:error, reason :: term}
|
| {:error, reason :: any}
|
||||||
when state: term
|
when state: any
|
||||||
|
|
||||||
@callback handle_event(event :: term, state :: term) ::
|
@callback handle_event(event :: term, state :: term) ::
|
||||||
{:ok, new_state}
|
{:ok, new_state}
|
||||||
@@ -511,8 +507,6 @@ defmodule GenEvent do
|
|||||||
{:ok, states, [name, handlers, hib]}
|
{:ok, states, [name, handlers, hib]}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Keeping deprecated format_status/2 since the current implementation is not
|
|
||||||
# compatible with format_status/1 and GenEvent is deprecated anyway
|
|
||||||
@doc false
|
@doc false
|
||||||
def format_status(opt, status_data) do
|
def format_status(opt, status_data) do
|
||||||
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
|
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule GenEvent.Stream do
|
defmodule GenEvent.Stream do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
@moduledoc deprecated: "This functionality is no longer supported"
|
|
||||||
defstruct manager: nil, timeout: :infinity
|
defstruct manager: nil, timeout: :infinity
|
||||||
|
|
||||||
@type t :: %__MODULE__{manager: GenEvent.manager(), timeout: timeout}
|
@type t :: %__MODULE__{manager: GenEvent.manager(), timeout: timeout}
|
||||||
@@ -51,9 +46,6 @@ defmodule GenEvent.Stream do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defimpl Enumerable, for: GenEvent.Stream do
|
defimpl Enumerable, for: GenEvent.Stream do
|
||||||
@moduledoc false
|
|
||||||
@moduledoc deprecated: "This functionality is no longer supported"
|
|
||||||
|
|
||||||
def reduce(stream, acc, fun) do
|
def reduce(stream, acc, fun) do
|
||||||
start_fun = fn -> start(stream) end
|
start_fun = fn -> start(stream) end
|
||||||
next_fun = &next(stream, &1)
|
next_fun = &next(stream, &1)
|
||||||
|
|||||||
+36
-145
@@ -1,7 +1,3 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
||||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
||||||
|
|
||||||
defmodule GenServer do
|
defmodule GenServer do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A behaviour module for implementing the server of a client-server relation.
|
A behaviour module for implementing the server of a client-server relation.
|
||||||
@@ -12,13 +8,6 @@ defmodule GenServer do
|
|||||||
will have a standard set of interface functions and include functionality for
|
will have a standard set of interface functions and include functionality for
|
||||||
tracing and error reporting. It will also fit into a supervision tree.
|
tracing and error reporting. It will also fit into a supervision tree.
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph BT
|
|
||||||
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
|
|
||||||
A & B & C -->|request| GenServer
|
|
||||||
GenServer -.->|reply| A & B & C
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The GenServer behaviour abstracts the common client-server interaction.
|
The GenServer behaviour abstracts the common client-server interaction.
|
||||||
@@ -147,44 +136,11 @@ defmodule GenServer do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
In practice, it is common to have both server and client functions in
|
In practice, it is common to have both server and client functions in
|
||||||
the same module. If the server and/or client implementations are growing
|
the same module. If the server and/or client implementations are growing
|
||||||
complex, you may want to have them in different modules.
|
complex, you may want to have them in different modules.
|
||||||
|
|
||||||
The following diagram summarizes the interactions between client and server.
|
|
||||||
Both Client and Server are processes and communication happens via messages
|
|
||||||
(continuous line). The Server <-> Module interaction happens when the
|
|
||||||
GenServer process calls your code (dotted lines):
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant C as Client (Process)
|
|
||||||
participant S as Server (Process)
|
|
||||||
participant M as Module (Code)
|
|
||||||
|
|
||||||
note right of C: Typically started by a supervisor
|
|
||||||
C->>+S: GenServer.start_link(module, arg, options)
|
|
||||||
S-->>+M: init(arg)
|
|
||||||
M-->>-S: {:ok, state} | :ignore | {:error, reason}
|
|
||||||
S->>-C: {:ok, pid} | :ignore | {:error, reason}
|
|
||||||
|
|
||||||
note right of C: call is synchronous
|
|
||||||
C->>+S: GenServer.call(pid, message)
|
|
||||||
S-->>+M: handle_call(message, from, state)
|
|
||||||
M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
|
|
||||||
S->>-C: reply
|
|
||||||
|
|
||||||
note right of C: cast is asynchronous
|
|
||||||
C-)S: GenServer.cast(pid, message)
|
|
||||||
S-->>+M: handle_cast(message, state)
|
|
||||||
M-->>-S: {:noreply, state} | {:stop, reason, state}
|
|
||||||
|
|
||||||
note right of C: send is asynchronous
|
|
||||||
C-)S: Kernel.send(pid, message)
|
|
||||||
S-->>+M: handle_info(message, state)
|
|
||||||
M-->>-S: {:noreply, state} | {:stop, reason, state}
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to supervise
|
## How to supervise
|
||||||
|
|
||||||
A `GenServer` is most commonly started under a supervision tree.
|
A `GenServer` is most commonly started under a supervision tree.
|
||||||
@@ -207,17 +163,14 @@ defmodule GenServer do
|
|||||||
The generated `child_spec/1` can be customized with the following options:
|
The generated `child_spec/1` can be customized with the following options:
|
||||||
|
|
||||||
* `:id` - the child specification identifier, defaults to the current module
|
* `:id` - the child specification identifier, defaults to the current module
|
||||||
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
|
* `:restart` - when the child should be restarted, defaults to `:permanent`
|
||||||
child should be restarted, defaults to `:permanent`
|
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
|
||||||
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
|
|
||||||
shut down the child, either immediately or by giving it time to shut down,
|
|
||||||
defaults to `5_000`
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
use GenServer, restart: :transient, shutdown: 10_000
|
use GenServer, restart: :transient, shutdown: 10_000
|
||||||
|
|
||||||
See the ["Child specification"](`m:Supervisor#module-child-specification`) section in the `Supervisor` module for more
|
See the "Child specification" section in the `Supervisor` module for more
|
||||||
detailed information. The `@doc` annotation immediately preceding
|
detailed information. The `@doc` annotation immediately preceding
|
||||||
`use GenServer` will be attached to the generated `child_spec/1` function.
|
`use GenServer` will be attached to the generated `child_spec/1` function.
|
||||||
|
|
||||||
@@ -232,8 +185,6 @@ defmodule GenServer do
|
|||||||
a name on start via the `:name` option. Registered names are also
|
a name on start via the `:name` option. Registered names are also
|
||||||
automatically cleaned up on termination. The supported values are:
|
automatically cleaned up on termination. The supported values are:
|
||||||
|
|
||||||
* `nil` (default) - the GenServer is not registered with a name.
|
|
||||||
|
|
||||||
* an atom - the GenServer is registered locally (to the current node)
|
* an atom - the GenServer is registered locally (to the current node)
|
||||||
with the given name using `Process.register/2`.
|
with the given name using `Process.register/2`.
|
||||||
|
|
||||||
@@ -274,14 +225,6 @@ defmodule GenServer do
|
|||||||
generated atoms won't be garbage-collected. For such cases, you can
|
generated atoms won't be garbage-collected. For such cases, you can
|
||||||
set up your own local registry by using the `Registry` module.
|
set up your own local registry by using the `Registry` module.
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
{:ok, _} = Registry.start_link(keys: :unique, name: :stacks)
|
|
||||||
name = {:via, Registry, {:stacks, "stack 1"}}
|
|
||||||
{:ok, _pid} = GenServer.start_link(Stack, "hello", name: name)
|
|
||||||
GenServer.whereis(name)
|
|
||||||
#=> #PID<0.150.0>
|
|
||||||
|
|
||||||
## Receiving "regular" messages
|
## Receiving "regular" messages
|
||||||
|
|
||||||
The goal of a `GenServer` is to abstract the "receive" loop for developers,
|
The goal of a `GenServer` is to abstract the "receive" loop for developers,
|
||||||
@@ -352,41 +295,6 @@ defmodule GenServer do
|
|||||||
message arriving, `handle_info/2` is called with `:timeout` as the first
|
message arriving, `handle_info/2` is called with `:timeout` as the first
|
||||||
argument.
|
argument.
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
defmodule Counter do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
@timeout to_timeout(second: 5)
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(count) do
|
|
||||||
{:ok, count, @timeout}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:increment, _from, count) do
|
|
||||||
new_count = count + 1
|
|
||||||
{:reply, new_count, new_count, @timeout}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info(:timeout, count) do
|
|
||||||
{:stop, :normal, count}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
|
|
||||||
after the initialization or after the last `:increment` call:
|
|
||||||
|
|
||||||
{:ok, counter_pid} = GenServer.start(Counter, 50)
|
|
||||||
GenServer.call(counter_pid, :increment)
|
|
||||||
#=> 51
|
|
||||||
|
|
||||||
# After 5 seconds
|
|
||||||
Process.alive?(counter_pid)
|
|
||||||
#=> false
|
|
||||||
|
|
||||||
## When (not) to use a GenServer
|
## When (not) to use a GenServer
|
||||||
|
|
||||||
So far, we have learned that a `GenServer` can be used as a supervised process
|
So far, we have learned that a `GenServer` can be used as a supervised process
|
||||||
@@ -452,7 +360,7 @@ defmodule GenServer do
|
|||||||
or is suspended, the parent PID, the debugger state, and the state of
|
or is suspended, the parent PID, the debugger state, and the state of
|
||||||
the behaviour module, which includes the callback module state
|
the behaviour module, which includes the callback module state
|
||||||
(as returned by `:sys.get_state/2`). It's possible to change how this
|
(as returned by `:sys.get_state/2`). It's possible to change how this
|
||||||
status is represented by defining the optional `c:GenServer.format_status/1`
|
status is represented by defining the optional `c:GenServer.format_status/2`
|
||||||
callback.
|
callback.
|
||||||
* `:sys.trace/3` - prints all the system events to `:stdio`.
|
* `:sys.trace/3` - prints all the system events to `:stdio`.
|
||||||
* `:sys.statistics/3` - manages collection of process statistics.
|
* `:sys.statistics/3` - manages collection of process statistics.
|
||||||
@@ -467,7 +375,7 @@ defmodule GenServer do
|
|||||||
Let's see how we could use those functions for debugging the stack server
|
Let's see how we could use those functions for debugging the stack server
|
||||||
we defined earlier.
|
we defined earlier.
|
||||||
|
|
||||||
iex> {:ok, pid} = Stack.start_link("")
|
iex> {:ok, pid} = Stack.start_link([])
|
||||||
iex> :sys.statistics(pid, true) # turn on collecting process statistics
|
iex> :sys.statistics(pid, true) # turn on collecting process statistics
|
||||||
iex> :sys.trace(pid, true) # turn on event printing
|
iex> :sys.trace(pid, true) # turn on event printing
|
||||||
iex> Stack.push(pid, 1)
|
iex> Stack.push(pid, 1)
|
||||||
@@ -523,10 +431,10 @@ defmodule GenServer do
|
|||||||
guide provides a tutorial-like introduction. The documentation and links
|
guide provides a tutorial-like introduction. The documentation and links
|
||||||
in Erlang can also provide extra insight.
|
in Erlang can also provide extra insight.
|
||||||
|
|
||||||
* [GenServer - Elixir's Getting Started Guide](genservers.md)
|
* [GenServer - Elixir's Getting Started Guide](https://elixir-lang.org/getting-started/mix-otp/genserver.html)
|
||||||
* [`:gen_server` module documentation](`:gen_server`)
|
* [`:gen_server` module documentation](`:gen_server`)
|
||||||
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
|
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -567,19 +475,16 @@ defmodule GenServer do
|
|||||||
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
|
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
|
||||||
after a delay to attempt a restart.
|
after a delay to attempt a restart.
|
||||||
|
|
||||||
Returning `{:error, reason}` will cause `start_link/3` to return
|
Returning `{:stop, reason}` will cause `start_link/3` to return
|
||||||
`{:error, reason}`.
|
`{:error, reason}` and the process to exit with reason `reason` without
|
||||||
|
entering the loop or calling `c:terminate/2`.
|
||||||
Returning `{:stop, reason}` will the process to exit with reason `reason`,
|
|
||||||
without entering the loop or calling `c:terminate/2`. `start_link/3` will
|
|
||||||
return `{:error, reason}`, but only if the caller is trapping exits.
|
|
||||||
"""
|
"""
|
||||||
@callback init(init_arg :: term) ::
|
@callback init(init_arg :: term) ::
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||||
| :ignore
|
| :ignore
|
||||||
| {:stop, reason :: term}
|
| {:stop, reason :: any}
|
||||||
when state: term
|
when state: any
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
|
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
|
||||||
@@ -814,39 +719,22 @@ defmodule GenServer do
|
|||||||
when old_vsn: term | {:down, term}
|
when old_vsn: term | {:down, term}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
This function is called by a `GenServer` process in the following situations:
|
Invoked in some cases to retrieve a formatted version of the `GenServer` status:
|
||||||
|
|
||||||
* [`:sys.get_status/1,2`](`:sys.get_status/1`) is invoked to get the `GenServer` status.
|
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
|
||||||
* The `GenServer` process terminates abnormally and logs an error.
|
status of the `GenServer`; in such cases, `reason` is `:normal`
|
||||||
|
|
||||||
This callback is used to limit the status of the process returned by
|
* the `GenServer` terminates abnormally and logs an error; in such cases,
|
||||||
[`:sys.get_status/1,2`](`:sys.get_status/1`) or sent to logger.
|
`reason` is `:terminate`
|
||||||
|
|
||||||
The callback gets a map `status` describing the current status and shall return
|
This callback can be useful to control the *appearance* of the status of the
|
||||||
a map `new_status` with the same keys, but it may transform some values.
|
`GenServer`. For example, it can be used to return a compact representation of
|
||||||
|
the `GenServer`'s state to avoid having large state terms printed.
|
||||||
Two possible use cases for this callback is to remove sensitive information
|
|
||||||
from the state to prevent it from being printed in log files, or to compact
|
|
||||||
large irrelevant status items that would only clutter the logs.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def format_status(status) do
|
|
||||||
Map.new(status, fn
|
|
||||||
{:state, state} -> {:state, Map.delete(state, :private_key)}
|
|
||||||
{:message, {:password, _}} -> {:message, {:password, "redacted"}}
|
|
||||||
key_value -> key_value
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
`pdict_and_state` is a two-elements list `[pdict, state]` where `pdict` is a
|
||||||
|
list of `{key, value}` tuples representing the current process dictionary of
|
||||||
|
the `GenServer` and `state` is the current state of the `GenServer`.
|
||||||
"""
|
"""
|
||||||
@doc since: "1.17.0"
|
|
||||||
@callback format_status(status :: :gen_server.format_status()) ::
|
|
||||||
new_status :: :gen_server.format_status()
|
|
||||||
|
|
||||||
# TODO: Remove this on v2.0
|
|
||||||
@doc deprecated: "Use format_status/1 callback instead"
|
|
||||||
@callback format_status(reason, pdict_and_state :: list) :: term
|
@callback format_status(reason, pdict_and_state :: list) :: term
|
||||||
when reason: :normal | :terminate
|
when reason: :normal | :terminate
|
||||||
|
|
||||||
@@ -855,7 +743,6 @@ defmodule GenServer do
|
|||||||
handle_info: 2,
|
handle_info: 2,
|
||||||
handle_cast: 2,
|
handle_cast: 2,
|
||||||
handle_call: 3,
|
handle_call: 3,
|
||||||
format_status: 1,
|
|
||||||
format_status: 2,
|
format_status: 2,
|
||||||
handle_continue: 2
|
handle_continue: 2
|
||||||
|
|
||||||
@@ -863,7 +750,7 @@ defmodule GenServer do
|
|||||||
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
||||||
|
|
||||||
@typedoc "The GenServer name"
|
@typedoc "The GenServer name"
|
||||||
@type name :: nil | atom | {:global, term} | {:via, module, term}
|
@type name :: atom | {:global, term} | {:via, module, term}
|
||||||
|
|
||||||
@typedoc "Options used by the `start*` functions"
|
@typedoc "Options used by the `start*` functions"
|
||||||
@type options :: [option]
|
@type options :: [option]
|
||||||
@@ -900,7 +787,7 @@ defmodule GenServer do
|
|||||||
quote location: :keep, bind_quoted: [opts: opts] do
|
quote location: :keep, bind_quoted: [opts: opts] do
|
||||||
@behaviour GenServer
|
@behaviour GenServer
|
||||||
|
|
||||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a specification to start this module under a supervisor.
|
Returns a specification to start this module under a supervisor.
|
||||||
|
|
||||||
@@ -1000,7 +887,7 @@ defmodule GenServer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defmacro __before_compile__(env) do
|
defmacro __before_compile__(env) do
|
||||||
if not Module.defines?(env.module, {:init, 1}) do
|
unless Module.defines?(env.module, {:init, 1}) do
|
||||||
message = """
|
message = """
|
||||||
function init/1 required by behaviour GenServer is not implemented \
|
function init/1 required by behaviour GenServer is not implemented \
|
||||||
(in module #{inspect(env.module)}).
|
(in module #{inspect(env.module)}).
|
||||||
@@ -1073,7 +960,7 @@ defmodule GenServer do
|
|||||||
or `:ignore`, the process is terminated and this function returns
|
or `:ignore`, the process is terminated and this function returns
|
||||||
`{:error, reason}` or `:ignore`, respectively.
|
`{:error, reason}` or `:ignore`, respectively.
|
||||||
"""
|
"""
|
||||||
@spec start_link(module, term, options) :: on_start
|
@spec start_link(module, any, options) :: on_start
|
||||||
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
||||||
do_start(:link, module, init_arg, options)
|
do_start(:link, module, init_arg, options)
|
||||||
end
|
end
|
||||||
@@ -1083,7 +970,7 @@ defmodule GenServer do
|
|||||||
|
|
||||||
See `start_link/3` for more information.
|
See `start_link/3` for more information.
|
||||||
"""
|
"""
|
||||||
@spec start(module, term, options) :: on_start
|
@spec start(module, any, options) :: on_start
|
||||||
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
||||||
do_start(:nolink, module, init_arg, options)
|
do_start(:nolink, module, init_arg, options)
|
||||||
end
|
end
|
||||||
@@ -1153,12 +1040,12 @@ defmodule GenServer do
|
|||||||
arrives or a timeout occurs. `c:handle_call/3` will be called on the server
|
arrives or a timeout occurs. `c:handle_call/3` will be called on the server
|
||||||
to handle the request.
|
to handle the request.
|
||||||
|
|
||||||
`server` can be a PID or any of the other values described in the
|
`server` can be any of the values described in the "Name registration"
|
||||||
"Name registration" section of the documentation for this module.
|
section of the documentation for this module.
|
||||||
|
|
||||||
## Timeouts
|
## Timeouts
|
||||||
|
|
||||||
`timeout` is a non-negative integer which specifies how many
|
`timeout` is an integer greater than zero which specifies how many
|
||||||
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
||||||
indefinitely. The default value is `5000`. If no reply is received within
|
indefinitely. The default value is `5000`. If no reply is received within
|
||||||
the specified time, the function call fails and the caller exits. If the
|
the specified time, the function call fails and the caller exits. If the
|
||||||
@@ -1175,6 +1062,10 @@ defmodule GenServer do
|
|||||||
nil ->
|
nil ->
|
||||||
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
|
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
|
||||||
|
|
||||||
|
# TODO: remove this clause when we require Erlang/OTP 25+
|
||||||
|
pid when pid == self() ->
|
||||||
|
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
|
||||||
|
|
||||||
pid ->
|
pid ->
|
||||||
try do
|
try do
|
||||||
:gen.call(pid, :"$gen_call", request, timeout)
|
:gen.call(pid, :"$gen_call", request, timeout)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user