Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server datetime fix #221

Merged
merged 8 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ globalThis.socket = io("https://127.0.0.1:8001", {
## Install the xlwings Python package from its repo into xlwings-server in editable mode

- xlwings: delete `project.toml` temporarily
- xlwings: replace `version = "dev"` with the version from `xlwings.js`
- xlwings: replace `version = "dev"` with `0.0.0`
- xlwings-server: `pip uninstall xlwings`
- xlwings-server: `cd ~/dev/xlwings && python setup.py develop`
- xlwings: undo deletion of `project.toml`
Expand Down
11 changes: 7 additions & 4 deletions app/custom_functions/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import asyncio
import datetime as dt
import sys
from pathlib import Path
from typing import Annotated
Expand Down Expand Up @@ -35,18 +36,20 @@ def hello(name):
@func
@arg("rows", doc="The number of rows in the returned array.")
@arg("cols", doc="The number of columns in the returned array.")
@ret(index=False)
def standard_normal(rows, cols):
"""Returns an array of standard normally distributed pseudo random numbers"""
rng = np.random.default_rng()
matrix = rng.standard_normal(size=(rows, cols))
df = pd.DataFrame(matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])])
date_rng = pd.date_range(start=dt.datetime(2025, 6, 15), periods=rows, freq="D")
df = pd.DataFrame(
matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])], index=date_rng
)
return df


# 3) Reading a pandas DataFrames
@func
@arg("df", pd.DataFrame, index=False)
@arg("df", pd.DataFrame)
def correl(df):
"""Like CORREL, but it works on whole matrices instead of just 2 arrays."""
return df.corr()
Expand All @@ -55,7 +58,7 @@ def correl(df):
# 4) Type hints: this is the same example as 3), but using type hints instead of
# decorators. You could also use type hints and decorators together. In this sample, we
# are storing the Annotated type hint outside of the function, so it is easy to reuse.
Df = Annotated[pd.DataFrame, {"index": False}]
Df = Annotated[pd.DataFrame, {"index": True}]


@func
Expand Down
2 changes: 1 addition & 1 deletion app/custom_scripts/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def show_alert(book: xw.Book):
def setup_custom_functions(book: xw.Book):
prefix = f"{settings.functions_namespace}"
if settings.environment != "prod":
prefix += f"_{settings.environment}"
prefix += f"_{settings.environment}".upper()
sheet = book.sheets.add()
sheet["A3"].value = f'={prefix}.HELLO("xlwings")'
sheet["A5"].value = f"={prefix}.STANDARD_NORMAL(3, 4)"
Expand Down
7 changes: 2 additions & 5 deletions app/static/js/core/custom-functions-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const debug = false;
let invocations = new Set();
let bodies = new Set();
let runtime;
let contentLanguage;
let socket = null;

Office.onReady(function (info) {
Expand Down Expand Up @@ -47,9 +46,6 @@ Office.onReady(function (info) {
} else {
runtime = "1.1";
}

// Content Language
contentLanguage = Office.context.contentLanguage;
});

function flattenVarargsArray(arr) {
Expand Down Expand Up @@ -166,7 +162,8 @@ async function base() {
func_name: funcName,
args: args,
caller_address: `${officeApiClient}[${workbookName}]${invocation.address}`, // not available for streaming functions
content_language: contentLanguage,
culture_info_name: await xlwings.getCultureInfoName(),
date_format: await xlwings.getDateFormat(),
version: "placeholder_xlwings_version",
runtime: runtime,
};
Expand Down
42 changes: 39 additions & 3 deletions app/static/js/core/xlwingsjs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,49 @@ export async function getActiveBookName() {
}
}

// Culture Info Name, e.g., en-DE
let cachedCultureInfoName = null;
export async function getCultureInfoName() {
if (cachedCultureInfoName) {
return cachedCultureInfoName;
}
if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) {
return null;
}
const context = new Excel.RequestContext();
context.application.cultureInfo.load(["name"]);
await context.sync();
cachedCultureInfoName = `${context.application.cultureInfo.name}`;
return cachedCultureInfoName;
}

// Date format
let cachedDateFormat = null;
export async function getDateFormat() {
if (cachedDateFormat) {
return cachedDateFormat;
}
if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) {
return null;
}
const context = new Excel.RequestContext();
context.application.cultureInfo.datetimeFormat.load(["shortDatePattern"]);
await context.sync();
cachedDateFormat = `${context.application.cultureInfo.datetimeFormat.shortDatePattern}`;
return cachedDateFormat;
}

export function printSupportedApiVersions() {
const versions = [...Array(30)].map((_, i) => `1.${i}`);

function printBuildInfo() {
async function printBuildInfo() {
if (Office.context.diagnostics) {
console.log(`Office Build: ${Office.context.diagnostics.version}`);
console.log(`Office Platform: ${Office.context.diagnostics.platform}`);
console.log(
`Culture Info Name: ${(await getCultureInfoName()) || "N/A"}`,
);
console.log(`Local Date Format: ${(await getDateFormat()) || "N/A"}`);
}
}

Expand All @@ -40,8 +76,8 @@ export function printSupportedApiVersions() {
}
}

Office.onReady(() => {
printBuildInfo();
Office.onReady(async () => {
await printBuildInfo();
const apiNames = [
"ExcelAPI",
"SharedRuntime",
Expand Down
47 changes: 9 additions & 38 deletions app/static/js/core/xlwingsjs/xlwings.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { xlAlert } from "./alert.js";
import { getAccessToken } from "./auth.js";
export { getAccessToken };
import { getActiveBookName, printSupportedApiVersions } from "./utils.js";
export { getActiveBookName };
import {
getActiveBookName,
printSupportedApiVersions,
getCultureInfoName,
getDateFormat,
} from "./utils.js";
export { getActiveBookName, getCultureInfoName, getDateFormat };

// Prints the supported API versions into the Console
printSupportedApiVersions();
Expand Down Expand Up @@ -37,6 +42,8 @@ const xlwings = {
getBookData,
runActions,
pyscriptAllDone,
getCultureInfoName,
getDateFormat,
};
globalThis.xlwings = xlwings;

Expand Down Expand Up @@ -593,42 +600,6 @@ Object.assign(globalThis.callbacks, funcs);

// Callbacks
async function setValues(context, action) {
// Handle DateTime (TODO: backend should deliver indices with datetime obj)
let dt;
let dtString;
action.values.forEach((valueRow, rowIndex) => {
valueRow.forEach((value, colIndex) => {
if (
typeof value === "string" &&
value.length > 18 &&
value.includes("T")
) {
dt = new Date(Date.parse(value));
// Excel on macOS does use the wrong locale if you set a custom one via
// macOS Settings > Date & Time > Open Language & Region > Apps
// as the date format seems to stick to the Region selected under General
// while toLocaleDateString then respects the specific selected language.
// Providing Office.context.contentLanguage fixes this but isn't available for
// Office Scripts
// https://learn.microsoft.com/en-us/office/dev/add-ins/develop/localization#match-datetime-format-with-client-locale
dtString = dt.toLocaleDateString(Office.context.contentLanguage);
// Note that adding the time will format the cell as Custom instead of Date/Time
// which xlwings currently doesn't translate to datetime when reading
if (dtString !== "Invalid Date") {
if (
dt.getHours() +
dt.getMinutes() +
dt.getSeconds() +
dt.getMilliseconds() !==
0
) {
dtString += " " + dt.toLocaleTimeString();
}
action.values[rowIndex][colIndex] = dtString;
}
}
});
});
let range = await getRange(context, action);
range.values = action.values;
await context.sync();
Expand Down
2 changes: 1 addition & 1 deletion docs/custom_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def pytoday():
return dt.date.today()
```

By default, it will format the date according to the content language of your Excel instance, but you can also override this by explicitly providing the `date_format` option:
By default, it will format the date according to the cultural info of your Excel instance, but you can also override this by explicitly providing the `date_format` option or the `XLWINGS_DATE_FORMAT` environment variable:

```python
import datetime as dt
Expand Down
6 changes: 3 additions & 3 deletions requirements-core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ aiocache==0.12.3
# via -r requirements-core.in
annotated-types==0.7.0
# via pydantic
anyio==4.7.0
anyio==4.8.0
# via
# httpx
# starlette
Expand Down Expand Up @@ -61,7 +61,7 @@ packaging==24.2 ; sys_platform != 'win32'
# via gunicorn
pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# fastapi-slim
# pydantic-settings
Expand Down Expand Up @@ -103,7 +103,7 @@ uvloop==0.21.0 ; sys_platform != 'win32'
# via -r requirements-core.in
wsproto==1.2.0
# via simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements-core.in

# The following packages were excluded from the output:
Expand Down
10 changes: 5 additions & 5 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ annotated-types==0.7.0
# via
# -r requirements.txt
# pydantic
anyio==4.7.0
anyio==4.8.0
# via
# -r requirements.txt
# httpx
Expand Down Expand Up @@ -70,7 +70,7 @@ httptools==0.6.4
# via -r requirements.txt
httpx==0.28.1
# via -r requirements.txt
identify==2.6.4
identify==2.6.5
# via pre-commit
idna==3.10
# via
Expand Down Expand Up @@ -118,7 +118,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via
# -r requirements.txt
# cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# -r requirements.txt
# fastapi-slim
Expand Down Expand Up @@ -194,15 +194,15 @@ uvicorn==0.34.0
# via -r requirements.txt
uvloop==0.21.0 ; sys_platform != 'win32'
# via -r requirements.txt
virtualenv==20.28.0
virtualenv==20.28.1
# via pre-commit
watchfiles==1.0.3
# via -r requirements-dev.in
wsproto==1.2.0
# via
# -r requirements.txt
# simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements.txt

# The following packages were excluded from the output:
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ annotated-types==0.7.0
# via
# -r requirements-core.txt
# pydantic
anyio==4.7.0
anyio==4.8.0
# via
# -r requirements-core.txt
# httpx
Expand Down Expand Up @@ -95,7 +95,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via
# -r requirements-core.txt
# cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# -r requirements-core.txt
# fastapi-slim
Expand Down Expand Up @@ -163,7 +163,7 @@ wsproto==1.2.0
# via
# -r requirements-core.txt
# simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements-core.txt

# The following packages were excluded from the output:
Expand Down
Loading